@circuitwall/jarela 2.4.0 → 2.5.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.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +1 -1
- package/.next/standalone/.next/build-manifest.json +2 -2
- package/.next/standalone/.next/prerender-manifest.json +4 -4
- package/.next/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +14 -2
- package/.next/standalone/.next/server/app/_not-found.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +16 -4
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/chats/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/lookup/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/pair/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/bridges/[id]/status/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/dashboard/pricing-task/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/sources/[id]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/documents/sources/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/events/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/agents/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/fill/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/refine/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extension/turn/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/config/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/extensions/tools/[name]/secrets/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/packages/defaults/[id]/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/reload/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/packages/route.js +28 -4
- package/.next/standalone/.next/server/app/api/v1/page-capture/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/providers/[provider]/models/route.js +578 -58
- package/.next/standalone/.next/server/app/api/v1/scheduled-tasks/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/route.js +118 -26
- package/.next/standalone/.next/server/app/api/v1/threads/[thread_id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/tools/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/update/apply/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/v1/watchers/[id]/run/route.js +1 -1
- package/.next/standalone/.next/server/app/api/v1/watchers/route.js +1 -1
- package/.next/standalone/.next/server/app/page.js +31202 -30960
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/setup/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +1 -1
- package/.next/standalone/.next/server/chunks/2471.js +17 -17
- package/.next/standalone/.next/server/chunks/2653.js +80 -1
- package/.next/standalone/.next/server/chunks/3399.js +10695 -10695
- package/.next/standalone/.next/server/chunks/3566.js +1 -1
- package/.next/standalone/.next/server/chunks/4189.js +1 -1
- package/.next/standalone/.next/server/chunks/4508.js +2 -2
- package/.next/standalone/.next/server/chunks/4596.js +28 -4
- package/.next/standalone/.next/server/chunks/46.js +18 -3
- package/.next/standalone/.next/server/chunks/466.js +53 -7
- package/.next/standalone/.next/server/chunks/4955.js +30 -3
- package/.next/standalone/.next/server/chunks/5585.js +28 -4
- package/.next/standalone/.next/server/chunks/6649.js +1 -1
- package/.next/standalone/.next/server/chunks/9164.js +65 -14
- package/.next/standalone/.next/server/chunks/9590.js +18 -3
- package/.next/standalone/.next/server/middleware-build-manifest.js +2 -2
- package/.next/standalone/.next/server/middleware.js +18 -3
- package/.next/standalone/.next/server/pages/404.html +14 -2
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/preview-props.json +3 -3
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/{1395-e4a99912b8f83044.js → 1395-bfe1f3ef760c0f50.js} +54 -8
- package/.next/standalone/.next/static/chunks/app/{layout-9bc0b10127cb7d42.js → layout-7b24ad13faa3944b.js} +69 -2
- package/.next/standalone/.next/static/chunks/app/{page-04748847c6ff866f.js → page-2ef8d75d40eb4311.js} +10661 -10352
- package/.next/standalone/.next/static/css/c160ea862133acd2.css +5 -0
- package/.next/standalone/docs/adoption/changesets.json +2756 -0
- package/.next/standalone/package.json +1 -1
- package/CHANGELOG.md +33 -0
- package/api/client.models.test.ts +10 -0
- package/api/client.startup.test.ts +129 -0
- package/api/client.ts +51 -9
- package/api/thread-metadata.test.ts +72 -0
- package/api/types.ts +6 -3
- package/app/api/v1/providers/[provider]/models/route.test.ts +216 -6
- package/app/api/v1/providers/[provider]/models/route.ts +259 -40
- package/app/api/v1/threads/[thread_id]/route.ts +26 -23
- package/app/globals.css +119 -29
- package/app/layout.tsx +12 -0
- package/components/agents/AgentsPanel.tsx +5 -13
- package/components/chat/ChatView.tsx +9 -18
- package/components/chat/InputBar.tsx +1 -1
- package/components/chat/MessageBubble.tsx +1 -1
- package/components/chat/MessageList.test.tsx +45 -4
- package/components/chat/MessageList.tsx +123 -89
- package/components/chat/ToolList.tsx +88 -84
- package/components/chat/useThreadActivity.test.tsx +2 -2
- package/components/chat/useThreadActivity.ts +2 -2
- package/components/chat/useThreadData.ts +1 -30
- package/components/chat/useThreadMetadataPolling.test.tsx +128 -0
- package/components/chat/useThreadMetadataPolling.ts +50 -0
- package/components/chat/useUserProfileAndAgent.test.tsx +31 -0
- package/components/chat/useUserProfileAndAgent.ts +13 -2
- package/components/dashboard/DashboardPanel.tsx +1 -1
- package/components/dashboard/Series.tsx +34 -21
- package/components/dashboard/SummaryCards.tsx +1 -1
- package/components/layout/AppShell.tsx +22 -7
- package/components/layout/MenuPanel.tsx +14 -24
- package/components/profile/ProfilePanel.tsx +2 -2
- package/components/settings/AppearancePanel.tsx +70 -3
- package/components/settings/SettingsPanel.tsx +2 -2
- package/components/tools/ToolsPanel.tsx +2 -7
- package/components/ui/AppearancePreferences.test.tsx +68 -0
- package/components/ui/BootScreen.test.tsx +44 -0
- package/components/ui/BootScreen.tsx +24 -11
- package/components/ui/NotificationStatus.test.tsx +2 -2
- package/components/ui/NotificationStatus.tsx +5 -5
- package/components/ui/SubTabBar.tsx +2 -3
- package/contexts/ThemeContext.tsx +84 -1
- package/hooks/useAgentSession.test.ts +12 -1
- package/hooks/useAgentSession.ts +6 -4
- package/hooks/useUrlSync.test.ts +13 -0
- package/hooks/useUrlSync.ts +0 -4
- package/lib/db/migrations.test.ts +61 -0
- package/lib/db/migrations.ts +13 -0
- package/lib/embeddings/performance.test.ts +114 -0
- package/lib/stores/dashboard-metrics-cache.test.ts +99 -0
- package/lib/stores/dashboard-metrics.ts +30 -4
- package/lib/stores/message-embedding-jobs.test.ts +27 -0
- package/lib/stores/message-embedding-jobs.ts +5 -3
- package/lib/stores/message-usage.test.ts +21 -1
- package/lib/stores/pricing-cache.test.ts +31 -0
- package/lib/stores/pricing.ts +3 -0
- package/lib/tools/packages/default-packages.test.ts +21 -0
- package/lib/tools/packages/optional-package.test.ts +72 -0
- package/lib/tools/packages/optional-package.ts +33 -4
- package/lib/ui/navigate.test.ts +13 -0
- package/lib/ui/navigate.ts +10 -2
- package/package.json +1 -1
- package/.next/standalone/.next/static/css/2669e8744cc8c212.css +0 -5
- /package/.next/standalone/.next/static/{hwujW4hR1b-r3k5WdMxZO → 544OFJV-q0RC6Gjgje4LT}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{hwujW4hR1b-r3k5WdMxZO → 544OFJV-q0RC6Gjgje4LT}/_ssgManifest.js +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { useEffect, useRef, useState } from "react";
|
|
3
|
-
import { Plus, Trash2 } from "lucide-react";
|
|
3
|
+
import { Bot, Plus, Trash2 } from "lucide-react";
|
|
4
4
|
import type { AgentConfig, AgentConfigIn } from "@/api/types";
|
|
5
5
|
import { api } from "@/api/client";
|
|
6
6
|
import { useAgents } from "@/hooks/useAgents";
|
|
7
7
|
import { useModels } from "@/hooks/useModels";
|
|
8
8
|
import { useDeepLinkScroll } from "@/hooks/useDeepLinkScroll";
|
|
9
|
-
import {
|
|
9
|
+
import { HeaderAction, PanelHeader } from "@/components/ui/PanelHeader";
|
|
10
10
|
import { AgentEditor } from "./AgentEditor";
|
|
11
11
|
import { useAppContext } from "@/contexts/AppContext";
|
|
12
12
|
|
|
@@ -100,17 +100,9 @@ export function AgentsPanel() {
|
|
|
100
100
|
return (
|
|
101
101
|
<div className="flex flex-col h-full min-h-0">
|
|
102
102
|
<>
|
|
103
|
-
{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<Button
|
|
107
|
-
onClick={() => setEditing("new")}
|
|
108
|
-
size="sm"
|
|
109
|
-
icon={<Plus size={11} />}
|
|
110
|
-
>
|
|
111
|
-
New
|
|
112
|
-
</Button>
|
|
113
|
-
</div>
|
|
103
|
+
<PanelHeader icon={<Bot size={14} />} title="Agents">
|
|
104
|
+
<HeaderAction onClick={() => setEditing("new")} icon={<Plus size={14} />} label="New" />
|
|
105
|
+
</PanelHeader>
|
|
114
106
|
|
|
115
107
|
{/* List */}
|
|
116
108
|
<div ref={containerRef} className="flex-1 overflow-y-auto px-2 pb-2">
|
|
@@ -17,6 +17,7 @@ import { useChatErrorReporting } from "./useChatErrorReporting";
|
|
|
17
17
|
import { useChatQueue, type ChatQueueApi } from "./useChatQueue";
|
|
18
18
|
import { useThreadCrossDeviceSync } from "./useThreadCrossDeviceSync";
|
|
19
19
|
import { useThreadData } from "./useThreadData";
|
|
20
|
+
import { useThreadMetadataPolling } from "./useThreadMetadataPolling";
|
|
20
21
|
import { useThreadActivity } from "./useThreadActivity";
|
|
21
22
|
import { useUserProfileAndAgent } from "./useUserProfileAndAgent";
|
|
22
23
|
import { composerPlaceholder, finalizeRunFromServer } from "./chat-run-utils";
|
|
@@ -109,21 +110,11 @@ export function ChatView({ threadId, agentId, sessionLoading, sessionError, onMe
|
|
|
109
110
|
const currentThreadIdRef = useRef(threadId);
|
|
110
111
|
currentThreadIdRef.current = threadId;
|
|
111
112
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
void api.threads.get(threadId, { limit: 1 }).then((d) => {
|
|
118
|
-
if (!cancelled) applyThreadMeta(thread.metaApplier, d);
|
|
119
|
-
}).catch(() => { /* best-effort */ });
|
|
120
|
-
}, 2000);
|
|
121
|
-
return () => {
|
|
122
|
-
cancelled = true;
|
|
123
|
-
window.clearInterval(timer);
|
|
124
|
-
};
|
|
125
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
126
|
-
}, [threadId, sse.streaming]);
|
|
113
|
+
useThreadMetadataPolling(
|
|
114
|
+
threadId,
|
|
115
|
+
sse.streaming || thread.warmSummaryPending || thread.compactionPending || thread.manualCompactionPending,
|
|
116
|
+
thread.metaApplier,
|
|
117
|
+
);
|
|
127
118
|
|
|
128
119
|
useThreadCrossDeviceSync({
|
|
129
120
|
threadId,
|
|
@@ -262,9 +253,9 @@ export function ChatView({ threadId, agentId, sessionLoading, sessionError, onMe
|
|
|
262
253
|
thread.metaApplier.setWarmSummaryTopics?.(parseWarmSummaryTopics(result.warm_summary_topics));
|
|
263
254
|
// The compact response carries only the chat recap; channel recaps
|
|
264
255
|
// (bridge, watcher, scheduled task) come with the thread metadata.
|
|
265
|
-
void api.threads.
|
|
256
|
+
void api.threads.metadata(compactedThreadId)
|
|
266
257
|
.then((d) => { if (stillCurrent()) applyThreadMeta(thread.metaApplier, d); })
|
|
267
|
-
.catch(() =>
|
|
258
|
+
.catch((err) => console.error("compaction metadata refresh failed", err));
|
|
268
259
|
}
|
|
269
260
|
notifyCompaction(result);
|
|
270
261
|
} catch (err) {
|
|
@@ -296,7 +287,7 @@ export function ChatView({ threadId, agentId, sessionLoading, sessionError, onMe
|
|
|
296
287
|
});
|
|
297
288
|
|
|
298
289
|
return (
|
|
299
|
-
<div className="flex
|
|
290
|
+
<div className="flex h-full min-h-0 w-full min-w-0 flex-col">
|
|
300
291
|
<MessageList
|
|
301
292
|
threadId={threadId}
|
|
302
293
|
messages={thread.messages}
|
|
@@ -366,7 +366,7 @@ export function InputBar({ attachments, onAttachmentsChange, onSubmit, onQueue,
|
|
|
366
366
|
)}
|
|
367
367
|
|
|
368
368
|
<textarea
|
|
369
|
-
className="flex-1 resize-none bg-surface-3/60 text-fg text-sm rounded-xl px-3 py-2 border border-border/60 focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent/40 placeholder:text-fg-faint max-h-[84px] min-h-[44px] transition-colors"
|
|
369
|
+
className="flex-1 min-w-0 resize-none bg-surface-3/60 text-fg text-sm rounded-xl px-3 py-2 border border-border/60 focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent/40 placeholder:text-fg-faint max-h-[84px] min-h-[44px] transition-colors"
|
|
370
370
|
ref={textareaRef}
|
|
371
371
|
placeholder={placeholder ?? "Message…"}
|
|
372
372
|
rows={1}
|
|
@@ -2040,7 +2040,7 @@ export const MessageBubble = memo(function MessageBubble({ message, attachedActi
|
|
|
2040
2040
|
)}
|
|
2041
2041
|
</div>
|
|
2042
2042
|
|
|
2043
|
-
<div className={`flex flex-col min-w-0 ${containsImageAttachment ? "max-w-[calc(100%-2.25rem)] sm:max-w-[92%] md:max-w-[86%]" : "max-w-[
|
|
2043
|
+
<div className={`flex flex-col min-w-0 ${!isUser ? "flex-1 items-start" : containsImageAttachment ? "max-w-[calc(100%-2.25rem)] sm:max-w-[92%] md:max-w-[86%] items-end" : "max-w-[calc(100%-2.25rem)] sm:max-w-[75%] items-end"}`}>
|
|
2044
2044
|
{(timeLabel || messageId) && (
|
|
2045
2045
|
<div className={`flex items-center gap-1 mb-0.5 px-1 opacity-0 group-hover:opacity-100 pointer-coarse:opacity-100 transition-opacity ${isUser ? "flex-row-reverse" : ""}`}>
|
|
2046
2046
|
{timeLabel && (
|
|
@@ -4,14 +4,16 @@ import { render, screen, fireEvent, within, waitFor } from "@testing-library/rea
|
|
|
4
4
|
import { MessageList } from "./MessageList";
|
|
5
5
|
import type { Message } from "@/api/types";
|
|
6
6
|
|
|
7
|
+
const renderCounts = vi.hoisted(() => new Map<string, number>());
|
|
7
8
|
vi.mock("./MessageBubble", () => ({
|
|
8
9
|
MessageBubble: ({ message, attachedActivity, showAvatar }: {
|
|
9
|
-
message: Pick<Message, "content">;
|
|
10
|
+
message: Pick<Message, "id" | "content">;
|
|
10
11
|
attachedActivity?: { source_kind: string };
|
|
11
12
|
showAvatar?: boolean;
|
|
12
|
-
}) =>
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
}) => {
|
|
14
|
+
renderCounts.set(message.id, (renderCounts.get(message.id) ?? 0) + 1);
|
|
15
|
+
return <div data-activity-kind={attachedActivity?.source_kind} data-show-avatar={String(showAvatar)}>{message.content}</div>;
|
|
16
|
+
},
|
|
15
17
|
}));
|
|
16
18
|
|
|
17
19
|
vi.mock("./ToolList", () => ({
|
|
@@ -65,6 +67,45 @@ function installPointerCapture(button: HTMLButtonElement) {
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
describe("MessageList conversation focus", () => {
|
|
70
|
+
it("does not re-render historical rows as streaming tokens or thinking arrive", () => {
|
|
71
|
+
renderCounts.clear();
|
|
72
|
+
const messages = [
|
|
73
|
+
mkMessage("history1", "user", "earlier prompt", "2026-10-18T08:00:00.000Z"),
|
|
74
|
+
mkMessage("history2", "assistant", "earlier answer", "2026-10-18T08:00:01.000Z"),
|
|
75
|
+
];
|
|
76
|
+
const { rerender } = render(
|
|
77
|
+
<MessageList messages={messages} streaming streamingContent="A" thinkingContent="thinking" />,
|
|
78
|
+
);
|
|
79
|
+
const firstCounts = messages.map((m) => renderCounts.get(m.id));
|
|
80
|
+
rerender(<MessageList messages={messages} streaming streamingContent="A longer reply" thinkingContent="more thinking" />);
|
|
81
|
+
expect(messages.map((m) => renderCounts.get(m.id))).toEqual(firstCounts);
|
|
82
|
+
expect(screen.getByText("A longer reply")).toBeTruthy();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("updates history when a persisted message changes", () => {
|
|
86
|
+
const messages = [mkMessage("history3", "assistant", "before edit", "2026-10-18T08:00:00.000Z")];
|
|
87
|
+
const { rerender } = render(<MessageList messages={messages} />);
|
|
88
|
+
rerender(<MessageList messages={[{ ...messages[0], content: "after edit" }]} />);
|
|
89
|
+
expect(screen.getByText("after edit")).toBeTruthy();
|
|
90
|
+
expect(screen.queryByText("before edit")).toBeNull();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("preserves historical message nodes when the context boundary moves", () => {
|
|
94
|
+
const messages = [
|
|
95
|
+
mkMessage("history4", "user", "first message", "2026-10-18T08:00:00.000Z"),
|
|
96
|
+
mkMessage("history5", "assistant", "second message", "2026-10-18T08:00:01.000Z"),
|
|
97
|
+
];
|
|
98
|
+
const { container, rerender } = render(<MessageList messages={messages} hotSinceSeq={4} />);
|
|
99
|
+
const first = container.querySelector('[data-message-id="history4"]');
|
|
100
|
+
const second = container.querySelector('[data-message-id="history5"]');
|
|
101
|
+
rerender(<MessageList messages={messages} hotSinceSeq={5} />);
|
|
102
|
+
expect(container.querySelector('[data-message-id="history4"]')).toBe(first);
|
|
103
|
+
expect(container.querySelector('[data-message-id="history5"]')).toBe(second);
|
|
104
|
+
const boundary = container.querySelector("[data-focus-boundary='1']");
|
|
105
|
+
expect(first!.compareDocumentPosition(boundary!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
106
|
+
expect(boundary!.compareDocumentPosition(second!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
107
|
+
});
|
|
108
|
+
|
|
68
109
|
it.each([
|
|
69
110
|
["bridge", "bridge-1:chat-1", "user", "bridge", { bridge_conversation: { key: "bridge-1:chat-1" } }],
|
|
70
111
|
["scheduled_task", "task-1", "assistant", "scheduled_task", undefined],
|
|
@@ -76,8 +76,9 @@ const ALL_FILTERS_SHOWN: Record<MessageFilterKey, boolean> = Object.fromEntries(
|
|
|
76
76
|
MESSAGE_FILTER_KEYS.map((k) => [k, true]),
|
|
77
77
|
) as Record<MessageFilterKey, boolean>;
|
|
78
78
|
const NO_CHANNEL_SUMMARIES: WarmChannelSummary[] = [];
|
|
79
|
+
const NOOP = () => {};
|
|
79
80
|
|
|
80
|
-
export function MessageList({ threadId, messages, steeredSegments, agentConfig, userProfile, streamingContent, thinkingContent, toolEvents, hasMore, loadingMore, onLoadMore, queuedMessages, onRemoveQueued, hotSinceSeq, warmSummary, warmSummaryBeforeSeq, warmSummaryComputedAt, warmSummarySourceMessages, warmSummarySourceChars, warmSummaryTopics, warmChannelSummaries = NO_CHANNEL_SUMMARIES, warmSummaryPending = false, compactionPending = false, onSetContextPin, streaming, contextWindowTokens, onRetryMessage, filters = ALL_FILTERS_SHOWN, onToggleFilter: toggle =
|
|
81
|
+
export function MessageList({ threadId, messages, steeredSegments, agentConfig, userProfile, streamingContent, thinkingContent, toolEvents, hasMore, loadingMore, onLoadMore, queuedMessages, onRemoveQueued, hotSinceSeq, warmSummary, warmSummaryBeforeSeq, warmSummaryComputedAt, warmSummarySourceMessages, warmSummarySourceChars, warmSummaryTopics, warmChannelSummaries = NO_CHANNEL_SUMMARIES, warmSummaryPending = false, compactionPending = false, onSetContextPin, streaming, contextWindowTokens, onRetryMessage, filters = ALL_FILTERS_SHOWN, onToggleFilter: toggle = NOOP, onResetFilters: reset = NOOP }: Props) {
|
|
81
82
|
const hostRef = useRef<HTMLDivElement>(null);
|
|
82
83
|
const maskRegionRef = useRef<HTMLDivElement>(null);
|
|
83
84
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
@@ -102,7 +103,11 @@ export function MessageList({ threadId, messages, steeredSegments, agentConfig,
|
|
|
102
103
|
const [pendingHotSinceSeq, setPendingHotSinceSeq] = useState<number | null>(null);
|
|
103
104
|
const [topControlsOpen, setTopControlsOpen] = useState(false);
|
|
104
105
|
const [summaryPopoverOpen, setSummaryPopoverOpen] = useState(false);
|
|
105
|
-
|
|
106
|
+
const hasStreamingContent = !!streamingContent;
|
|
107
|
+
const hasThinkingContent = !!thinkingContent;
|
|
108
|
+
const hasLiveToolEvents = !!toolEvents?.length;
|
|
109
|
+
const hidePersistedDraft = !!streaming || hasStreamingContent;
|
|
110
|
+
|
|
106
111
|
const retryWorkflowStep = useMemo(
|
|
107
112
|
() => onRetryMessage
|
|
108
113
|
? (request: WorkflowStepRetryRequest) => onRetryMessage(workflowStepRetryMessage(request), [])
|
|
@@ -116,7 +121,7 @@ export function MessageList({ threadId, messages, steeredSegments, agentConfig,
|
|
|
116
121
|
// they're handled below by gating the inline ToolList / ThinkingLine.
|
|
117
122
|
const visibleMessages = useMemo(() => {
|
|
118
123
|
return messages.filter((m) => {
|
|
119
|
-
if (m.role === "assistant" && m.transcript_status === "in_progress" &&
|
|
124
|
+
if (m.role === "assistant" && m.transcript_status === "in_progress" && hidePersistedDraft) return false;
|
|
120
125
|
const cat = m.category;
|
|
121
126
|
if (!cat) return true;
|
|
122
127
|
if (cat === "scheduled_task") return filters.scheduled_task;
|
|
@@ -129,7 +134,53 @@ export function MessageList({ threadId, messages, steeredSegments, agentConfig,
|
|
|
129
134
|
// never silently drop content the server thinks should be visible.
|
|
130
135
|
return true;
|
|
131
136
|
});
|
|
132
|
-
}, [messages,
|
|
137
|
+
}, [messages, hidePersistedDraft, filters.scheduled_task, filters.watcher, filters.bridge, filters.extension, filters.page_capture, filters.synthetic]);
|
|
138
|
+
|
|
139
|
+
const historyRows = useMemo(() => {
|
|
140
|
+
const attachedActivities = new Map<string, AutomationActivityMetadata>();
|
|
141
|
+
const hiddenActivityIds = new Set<string>();
|
|
142
|
+
for (let i = 0; i < visibleMessages.length - 1; i++) {
|
|
143
|
+
const activityMessage = visibleMessages[i];
|
|
144
|
+
const activity = activityMessage.metadata?.automation_activity;
|
|
145
|
+
if (!activity) continue;
|
|
146
|
+
const related = visibleMessages[i + 1];
|
|
147
|
+
const matches = !related.metadata?.automation_activity && (
|
|
148
|
+
activity.source_kind === "bridge"
|
|
149
|
+
? related.role === "user"
|
|
150
|
+
&& related.metadata?.bridge_conversation?.key === activity.source_id
|
|
151
|
+
: related.role === "assistant" && related.category === activity.source_kind
|
|
152
|
+
);
|
|
153
|
+
if (!matches) continue;
|
|
154
|
+
attachedActivities.set(related.id, activity);
|
|
155
|
+
hiddenActivityIds.add(activityMessage.id);
|
|
156
|
+
}
|
|
157
|
+
const segmentsById = new Map(steeredSegments?.map((segment) => [segment.id, segment]));
|
|
158
|
+
let previousRole: Message["role"] | null = null;
|
|
159
|
+
return visibleMessages.map((msg, i) => {
|
|
160
|
+
if (hiddenActivityIds.has(msg.id)) return null;
|
|
161
|
+
const startsTurn = previousRole == null || previousRole !== msg.role;
|
|
162
|
+
previousRole = msg.role;
|
|
163
|
+
const steeredSegment = segmentsById.get(msg.id);
|
|
164
|
+
return (
|
|
165
|
+
<HistoryRow
|
|
166
|
+
key={msg.id}
|
|
167
|
+
message={msg}
|
|
168
|
+
steeredSegment={steeredSegment}
|
|
169
|
+
segmentShowAvatar={i === 0 || visibleMessages[i - 1].role !== "assistant"}
|
|
170
|
+
attachedActivity={attachedActivities.get(msg.id)}
|
|
171
|
+
startsTurn={startsTurn}
|
|
172
|
+
separated={startsTurn && i > 0}
|
|
173
|
+
threadId={threadId ?? null}
|
|
174
|
+
agentConfig={agentConfig}
|
|
175
|
+
userProfile={userProfile}
|
|
176
|
+
showToolEvents={filters.tool_use}
|
|
177
|
+
contextWindowTokens={contextWindowTokens ?? null}
|
|
178
|
+
isLatest={i === visibleMessages.length - 1 && !hasStreamingContent}
|
|
179
|
+
onRetry={onRetryMessage}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
}, [visibleMessages, steeredSegments, threadId, agentConfig, userProfile, filters.tool_use, contextWindowTokens, hasStreamingContent, onRetryMessage]);
|
|
133
184
|
|
|
134
185
|
const hiddenCount = messages.length - visibleMessages.length;
|
|
135
186
|
const effectiveHotSinceSeq = hotSinceSeq ?? null;
|
|
@@ -536,7 +587,7 @@ export function MessageList({ threadId, messages, steeredSegments, agentConfig,
|
|
|
536
587
|
// perceive this as "chat is empty". Auto-reset once per thread snapshot so
|
|
537
588
|
// content remains visible without requiring manual recovery.
|
|
538
589
|
useEffect(() => {
|
|
539
|
-
if (messages.length === 0 ||
|
|
590
|
+
if (messages.length === 0 || hasStreamingContent) return;
|
|
540
591
|
if (visibleMessages.length > 0) return;
|
|
541
592
|
if (hiddenCount !== messages.length) return;
|
|
542
593
|
const lastId = messages[messages.length - 1]?.id ?? "none";
|
|
@@ -544,7 +595,7 @@ export function MessageList({ threadId, messages, steeredSegments, agentConfig,
|
|
|
544
595
|
if (autoRecoveredRef.current === key) return;
|
|
545
596
|
autoRecoveredRef.current = key;
|
|
546
597
|
reset();
|
|
547
|
-
}, [threadId, messages, visibleMessages.length, hiddenCount,
|
|
598
|
+
}, [threadId, messages, visibleMessages.length, hiddenCount, hasStreamingContent, reset]);
|
|
548
599
|
|
|
549
600
|
// Surface a category chip in the toolbar only if it can actually do
|
|
550
601
|
// something useful right now: scheduled_task/bridge/synthetic chips
|
|
@@ -563,10 +614,10 @@ export function MessageList({ threadId, messages, steeredSegments, agentConfig,
|
|
|
563
614
|
else if (m.category === "synthetic") set.add("synthetic");
|
|
564
615
|
if (m.tool_events && m.tool_events.length > 0) set.add("tool_use");
|
|
565
616
|
}
|
|
566
|
-
if (
|
|
567
|
-
if (
|
|
617
|
+
if (hasThinkingContent) set.add("thinking");
|
|
618
|
+
if (hasLiveToolEvents) set.add("tool_use");
|
|
568
619
|
return MESSAGE_FILTER_KEYS.filter((k) => set.has(k));
|
|
569
|
-
}, [messages,
|
|
620
|
+
}, [messages, hasThinkingContent, hasLiveToolEvents]);
|
|
570
621
|
|
|
571
622
|
// Tracks whether the user was at the bottom on the most recent scroll event.
|
|
572
623
|
// After every render, if true, we snap to bottom — which means: while the
|
|
@@ -920,30 +971,6 @@ export function MessageList({ threadId, messages, steeredSegments, agentConfig,
|
|
|
920
971
|
// "+ N more we don't know about yet" — older pages are likely unloaded.
|
|
921
972
|
const olderCountLabel = olderInVisible + (hasMore ? 1 : 0);
|
|
922
973
|
|
|
923
|
-
const attachedActivities = new Map<string, AutomationActivityMetadata>();
|
|
924
|
-
const hiddenActivityIds = new Set<string>();
|
|
925
|
-
for (let i = 0; i < visibleMessages.length - 1; i++) {
|
|
926
|
-
const activityMessage = visibleMessages[i];
|
|
927
|
-
const activity = activityMessage.metadata?.automation_activity;
|
|
928
|
-
if (!activity) continue;
|
|
929
|
-
const related = visibleMessages[i + 1];
|
|
930
|
-
const matches = !related.metadata?.automation_activity && (
|
|
931
|
-
activity.source_kind === "bridge"
|
|
932
|
-
? related.role === "user"
|
|
933
|
-
&& related.metadata?.bridge_conversation?.key === activity.source_id
|
|
934
|
-
: related.role === "assistant" && related.category === activity.source_kind
|
|
935
|
-
);
|
|
936
|
-
if (!matches) continue;
|
|
937
|
-
attachedActivities.set(related.id, activity);
|
|
938
|
-
hiddenActivityIds.add(activityMessage.id);
|
|
939
|
-
}
|
|
940
|
-
const previousVisibleRoles = new Map<string, Message["role"] | null>();
|
|
941
|
-
let previousVisibleRole: Message["role"] | null = null;
|
|
942
|
-
for (const message of visibleMessages) {
|
|
943
|
-
previousVisibleRoles.set(message.id, previousVisibleRole);
|
|
944
|
-
if (!hiddenActivityIds.has(message.id)) previousVisibleRole = message.role;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
974
|
const renderBoundary = (key: string) => (
|
|
948
975
|
<div
|
|
949
976
|
key={key}
|
|
@@ -969,54 +996,12 @@ export function MessageList({ threadId, messages, steeredSegments, agentConfig,
|
|
|
969
996
|
</div>
|
|
970
997
|
);
|
|
971
998
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
const previousRole = previousVisibleRoles.get(msg.id);
|
|
979
|
-
const startsTurn = previousRole == null || previousRole !== msg.role;
|
|
980
|
-
const steeredSegment = steeredSegments?.find((segment) => segment.id === msg.id);
|
|
981
|
-
if (steeredSegment) {
|
|
982
|
-
out.push(
|
|
983
|
-
<StreamingBubble
|
|
984
|
-
key={`steered-${steeredSegment.id}`}
|
|
985
|
-
content={steeredSegment.content}
|
|
986
|
-
threadId={threadId ?? null}
|
|
987
|
-
agentConfig={agentConfig ?? null}
|
|
988
|
-
showAvatar={i === 0 || visibleMessages[i - 1].role !== "assistant"}
|
|
989
|
-
/>,
|
|
990
|
-
);
|
|
991
|
-
}
|
|
992
|
-
out.push(
|
|
993
|
-
<div
|
|
994
|
-
key={msg.id}
|
|
995
|
-
id={typeof msg.seq === "number" ? String(msg.seq) : `msg-${msg.id}`}
|
|
996
|
-
data-message-id={msg.id}
|
|
997
|
-
data-hot-candidate="1"
|
|
998
|
-
data-message-seq={msg.seq}
|
|
999
|
-
data-created-at={msg.created_at}
|
|
1000
|
-
className={startsTurn && i > 0 ? "mt-3" : undefined}
|
|
1001
|
-
>
|
|
1002
|
-
<MessageBubble
|
|
1003
|
-
message={msg}
|
|
1004
|
-
attachedActivity={attachedActivities.get(msg.id)}
|
|
1005
|
-
threadId={threadId ?? null}
|
|
1006
|
-
agentConfig={agentConfig}
|
|
1007
|
-
userProfile={userProfile}
|
|
1008
|
-
showAvatar={startsTurn}
|
|
1009
|
-
showToolEvents={filters.tool_use}
|
|
1010
|
-
contextWindowTokens={contextWindowTokens ?? null}
|
|
1011
|
-
isLatest={i === visibleMessages.length - 1 && !streamingContent}
|
|
1012
|
-
onRetry={onRetryMessage}
|
|
1013
|
-
/>
|
|
1014
|
-
</div>,
|
|
1015
|
-
);
|
|
1016
|
-
return out;
|
|
1017
|
-
});
|
|
1018
|
-
if (hasBoundary && pinAfterAll) nodes.push(renderBoundary("boundary-tail"));
|
|
1019
|
-
return nodes;
|
|
999
|
+
if (!hasBoundary) return historyRows;
|
|
1000
|
+
return [
|
|
1001
|
+
...historyRows.slice(0, boundaryIndex),
|
|
1002
|
+
renderBoundary(pinAfterAll ? "boundary-tail" : `boundary-${visibleMessages[boundaryIndex].id}`),
|
|
1003
|
+
...historyRows.slice(boundaryIndex),
|
|
1004
|
+
];
|
|
1020
1005
|
})()}
|
|
1021
1006
|
<LiveTurnActivity
|
|
1022
1007
|
thinkingContent={thinkingContent}
|
|
@@ -1225,8 +1210,8 @@ function LiveTurnActivity({
|
|
|
1225
1210
|
if (!showThinking && !streamingContent && !showTools && !showDots) return null;
|
|
1226
1211
|
return (
|
|
1227
1212
|
<div data-testid="live-turn-activity" className="mt-1 flex w-full min-w-0 flex-col gap-1">
|
|
1228
|
-
{showDots && <ThinkingDots
|
|
1229
|
-
{showThinking && <ThinkingLine text={thinkingContent}
|
|
1213
|
+
{showDots && <div className="min-w-0 pl-9"><ThinkingDots /></div>}
|
|
1214
|
+
{showThinking && <div className="min-w-0 pl-9"><ThinkingLine text={thinkingContent} /></div>}
|
|
1230
1215
|
{streamingContent && (
|
|
1231
1216
|
<StreamingBubble
|
|
1232
1217
|
content={streamingContent}
|
|
@@ -1235,16 +1220,65 @@ function LiveTurnActivity({
|
|
|
1235
1220
|
showAvatar={showStreamingAvatar}
|
|
1236
1221
|
/>
|
|
1237
1222
|
)}
|
|
1238
|
-
{showTools && <ToolList events={toolEvents}
|
|
1223
|
+
{showTools && <div className="min-w-0 pl-9"><ToolList events={toolEvents} /></div>}
|
|
1239
1224
|
</div>
|
|
1240
1225
|
);
|
|
1241
1226
|
}
|
|
1242
1227
|
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1228
|
+
const HistoryRow = memo(function HistoryRow({
|
|
1229
|
+
message, steeredSegment, segmentShowAvatar, attachedActivity, startsTurn, separated,
|
|
1230
|
+
threadId, agentConfig, userProfile, showToolEvents, contextWindowTokens, isLatest, onRetry,
|
|
1231
|
+
}: {
|
|
1232
|
+
message: Message;
|
|
1233
|
+
steeredSegment?: SteeredSegment;
|
|
1234
|
+
segmentShowAvatar: boolean;
|
|
1235
|
+
attachedActivity?: AutomationActivityMetadata;
|
|
1236
|
+
startsTurn: boolean;
|
|
1237
|
+
separated: boolean;
|
|
1238
|
+
threadId: string | null;
|
|
1239
|
+
agentConfig?: AgentConfig | null;
|
|
1240
|
+
userProfile?: UserProfile | null;
|
|
1241
|
+
showToolEvents: boolean;
|
|
1242
|
+
contextWindowTokens: number | null;
|
|
1243
|
+
isLatest: boolean;
|
|
1244
|
+
onRetry?: Props["onRetryMessage"];
|
|
1245
|
+
}) {
|
|
1246
|
+
return (
|
|
1247
|
+
<>
|
|
1248
|
+
{steeredSegment && (
|
|
1249
|
+
<StreamingBubble
|
|
1250
|
+
content={steeredSegment.content}
|
|
1251
|
+
threadId={threadId}
|
|
1252
|
+
agentConfig={agentConfig ?? null}
|
|
1253
|
+
showAvatar={segmentShowAvatar}
|
|
1254
|
+
/>
|
|
1255
|
+
)}
|
|
1256
|
+
<div
|
|
1257
|
+
id={typeof message.seq === "number" ? String(message.seq) : `msg-${message.id}`}
|
|
1258
|
+
data-message-id={message.id}
|
|
1259
|
+
data-hot-candidate="1"
|
|
1260
|
+
data-message-seq={message.seq}
|
|
1261
|
+
data-created-at={message.created_at}
|
|
1262
|
+
className={separated ? "mt-3" : undefined}
|
|
1263
|
+
>
|
|
1264
|
+
<MessageBubble
|
|
1265
|
+
message={message}
|
|
1266
|
+
attachedActivity={attachedActivity}
|
|
1267
|
+
threadId={threadId}
|
|
1268
|
+
agentConfig={agentConfig}
|
|
1269
|
+
userProfile={userProfile}
|
|
1270
|
+
showAvatar={startsTurn}
|
|
1271
|
+
showToolEvents={showToolEvents}
|
|
1272
|
+
contextWindowTokens={contextWindowTokens}
|
|
1273
|
+
isLatest={isLatest}
|
|
1274
|
+
onRetry={onRetry}
|
|
1275
|
+
/>
|
|
1276
|
+
</div>
|
|
1277
|
+
</>
|
|
1278
|
+
);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
// Keep the synthetic message identity stable when only the parent updates.
|
|
1248
1282
|
const StreamingBubble = memo(function StreamingBubble({
|
|
1249
1283
|
content,
|
|
1250
1284
|
threadId,
|