diff --git a/frontend/src/components/assistant-ui/reasoning.tsx b/frontend/src/components/assistant-ui/reasoning.tsx
index 3dd2585d2e..e43eb2ca4b 100644
--- a/frontend/src/components/assistant-ui/reasoning.tsx
+++ b/frontend/src/components/assistant-ui/reasoning.tsx
@@ -165,7 +165,7 @@ function ReasoningTrigger({
Thinking...
) : (
- Thought for {duration ?? 0}s
+ Thought for {duration ?? 0} seconds
)}
= startIndex && lastIndex <= endIndex;
});
+ const persistedDuration = useAuiState(({ message }) => {
+ const d = (message.metadata?.custom as Record)
+ ?.reasoningDuration;
+ return typeof d === "number" ? d : 0;
+ });
+
const [manualOpen, setManualOpen] = useState(false);
const [duration, setDuration] = useState(0);
const startTimeRef = useRef(null);
@@ -319,7 +325,7 @@ const ReasoningGroupImpl: ReasoningGroupComponent = ({
>
{
const UserActionBar: FC = () => {
return (
+
+
+ message.isCopied}>
+
+
+ !message.isCopied}>
+
+
+
+
-
+
@@ -347,6 +359,10 @@ const UserActionBar: FC = () => {
};
const EditComposer: FC = () => {
+ const threadRuntime = useThreadRuntime();
+ const composerRuntime = useComposerRuntime();
+ const messageRuntime = useMessageRuntime();
+
return (
@@ -360,9 +376,25 @@ const EditComposer: FC = () => {
Cancel
-
-
-
+
diff --git a/frontend/src/features/chat/adapter.ts b/frontend/src/features/chat/adapter.ts
index 3dcf881ad2..c4e103f62d 100644
--- a/frontend/src/features/chat/adapter.ts
+++ b/frontend/src/features/chat/adapter.ts
@@ -65,6 +65,9 @@ export function createStreamAdapter(apiUrl: string = API): ChatModelAdapter {
}
const decoder = new TextDecoder();
let text = "";
+ let reasoningStart: number | null = null;
+ let reasoningDuration = 0;
+
while (true) {
const { done, value } = await reader.read();
if (done) {
@@ -72,8 +75,21 @@ export function createStreamAdapter(apiUrl: string = API): ChatModelAdapter {
}
text += decoder.decode(value, { stream: true });
const parts = parseThinkTags(text) ?? [];
+
+ if (parts.some((p) => p.type === "reasoning") && !reasoningStart) {
+ reasoningStart = Date.now();
+ }
+ if (text.includes("") && reasoningStart && !reasoningDuration) {
+ reasoningDuration = Math.round(
+ (Date.now() - reasoningStart) / 1000,
+ );
+ }
+
if (parts.length > 0) {
- yield { content: parts };
+ yield {
+ content: parts,
+ metadata: { custom: { reasoningDuration } },
+ };
}
}
},
diff --git a/frontend/src/features/chat/runtime-provider.tsx b/frontend/src/features/chat/runtime-provider.tsx
index d411d595f8..593fe42b31 100644
--- a/frontend/src/features/chat/runtime-provider.tsx
+++ b/frontend/src/features/chat/runtime-provider.tsx
@@ -52,7 +52,7 @@ function toThreadMessage(m: MessageRecord): ThreadMessage {
role: "assistant" as const,
status: { type: "complete" as const, reason: "unknown" as const },
metadata: {
- custom: {},
+ custom: (m.metadata as Record) ?? {},
steps: [],
unstable_annotations: [],
unstable_data: [],
@@ -183,11 +183,13 @@ function ThreadHistoryProvider({
const content = Array.isArray(message.content)
? JSON.parse(JSON.stringify(message.content))
: [];
+ const custom = message.metadata?.custom;
await db.messages.put({
id: message.id,
threadId: remoteId,
role: message.role,
content,
+ ...(custom && Object.keys(custom).length > 0 && { metadata: custom }),
createdAt: message.createdAt?.getTime() ?? Date.now(),
});
},
diff --git a/frontend/src/features/chat/types.ts b/frontend/src/features/chat/types.ts
index 655aba7095..2ef9b6fdc5 100644
--- a/frontend/src/features/chat/types.ts
+++ b/frontend/src/features/chat/types.ts
@@ -18,6 +18,7 @@ export interface MessageRecord {
threadId: string;
role: import("@assistant-ui/react").ThreadMessage["role"];
content: import("@assistant-ui/react").ThreadMessage["content"];
+ metadata?: Record;
createdAt: number;
}