@springbrand/message-panel 0.1.2 → 0.1.3-alpha.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.
Files changed (99) hide show
  1. package/demo/camel-chat-showcase.tsx +1265 -0
  2. package/demo/fixtures.ts +292 -0
  3. package/demo/message-panel-chat-showcase.tsx +250 -0
  4. package/demo/message-panel-gallery.tsx +389 -0
  5. package/package.json +6 -16
  6. package/src/adapters/ai-sdk.ts +32 -0
  7. package/src/camel/camel-chat-messages.tsx +1423 -0
  8. package/src/camel/camel-model-select.tsx +88 -0
  9. package/src/camel/camel-plan-bar.tsx +115 -0
  10. package/src/camel/camel-prompt-input.tsx +547 -0
  11. package/src/camel/camel-tool-presentation.tsx +1270 -0
  12. package/src/camel/camel-turn.ts +316 -0
  13. package/src/camel/turn-summary-bar.tsx +81 -0
  14. package/src/chat-summary-panel.tsx +229 -0
  15. package/src/composer/chat-composer.tsx +216 -0
  16. package/src/composer/composer-attachments.tsx +138 -0
  17. package/src/composer/composer-trigger-popover.tsx +236 -0
  18. package/src/composer/composer.tsx +425 -0
  19. package/src/composer/drop-controller.ts +80 -0
  20. package/src/composer/types.ts +79 -0
  21. package/src/contracts.ts +138 -0
  22. package/src/conversation.tsx +76 -0
  23. package/src/internal/cn.ts +3 -0
  24. package/src/message-panel.tsx +368 -0
  25. package/src/message.tsx +254 -0
  26. package/src/parts/index.tsx +1149 -0
  27. package/src/parts/kind.ts +67 -0
  28. package/src/parts/plan.ts +63 -0
  29. package/src/parts/primitives.tsx +439 -0
  30. package/src/registry.ts +44 -0
  31. package/src/status.tsx +132 -0
  32. package/dist/demo/camel-chat-showcase.d.ts +0 -1
  33. package/dist/demo/camel-chat-showcase.js +0 -1007
  34. package/dist/demo/fixtures.d.ts +0 -32
  35. package/dist/demo/fixtures.js +0 -250
  36. package/dist/demo/index.js +0 -4
  37. package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
  38. package/dist/demo/message-panel-chat-showcase.js +0 -114
  39. package/dist/demo/message-panel-gallery.d.ts +0 -1
  40. package/dist/demo/message-panel-gallery.js +0 -130
  41. package/dist/src/adapters/ai-sdk.d.ts +0 -4
  42. package/dist/src/adapters/ai-sdk.js +0 -25
  43. package/dist/src/camel/camel-chat-messages.d.ts +0 -44
  44. package/dist/src/camel/camel-chat-messages.js +0 -421
  45. package/dist/src/camel/camel-model-select.d.ts +0 -13
  46. package/dist/src/camel/camel-model-select.js +0 -7
  47. package/dist/src/camel/camel-plan-bar.d.ts +0 -5
  48. package/dist/src/camel/camel-plan-bar.js +0 -24
  49. package/dist/src/camel/camel-prompt-input.d.ts +0 -32
  50. package/dist/src/camel/camel-prompt-input.js +0 -182
  51. package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
  52. package/dist/src/camel/camel-tool-presentation.js +0 -784
  53. package/dist/src/camel/camel-turn.d.ts +0 -57
  54. package/dist/src/camel/camel-turn.js +0 -203
  55. package/dist/src/camel/index.js +0 -7
  56. package/dist/src/camel/turn-summary-bar.d.ts +0 -8
  57. package/dist/src/camel/turn-summary-bar.js +0 -12
  58. package/dist/src/chat-summary-panel.d.ts +0 -22
  59. package/dist/src/chat-summary-panel.js +0 -26
  60. package/dist/src/composer/chat-composer.d.ts +0 -4
  61. package/dist/src/composer/chat-composer.js +0 -90
  62. package/dist/src/composer/composer-attachments.d.ts +0 -13
  63. package/dist/src/composer/composer-attachments.js +0 -47
  64. package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
  65. package/dist/src/composer/composer-trigger-popover.js +0 -29
  66. package/dist/src/composer/composer.d.ts +0 -15
  67. package/dist/src/composer/composer.js +0 -180
  68. package/dist/src/composer/drop-controller.d.ts +0 -8
  69. package/dist/src/composer/drop-controller.js +0 -69
  70. package/dist/src/composer/index.js +0 -6
  71. package/dist/src/composer/types.d.ts +0 -62
  72. package/dist/src/composer/types.js +0 -1
  73. package/dist/src/contracts.d.ts +0 -94
  74. package/dist/src/contracts.js +0 -1
  75. package/dist/src/conversation.d.ts +0 -13
  76. package/dist/src/conversation.js +0 -18
  77. package/dist/src/index.js +0 -12
  78. package/dist/src/internal/cn.d.ts +0 -1
  79. package/dist/src/internal/cn.js +0 -3
  80. package/dist/src/message-panel.d.ts +0 -4
  81. package/dist/src/message-panel.js +0 -144
  82. package/dist/src/message.d.ts +0 -36
  83. package/dist/src/message.js +0 -83
  84. package/dist/src/parts/index.d.ts +0 -187
  85. package/dist/src/parts/index.js +0 -332
  86. package/dist/src/parts/kind.d.ts +0 -3
  87. package/dist/src/parts/kind.js +0 -69
  88. package/dist/src/parts/plan.d.ts +0 -10
  89. package/dist/src/parts/plan.js +0 -40
  90. package/dist/src/parts/primitives.d.ts +0 -70
  91. package/dist/src/parts/primitives.js +0 -153
  92. package/dist/src/registry.d.ts +0 -4
  93. package/dist/src/registry.js +0 -31
  94. package/dist/src/status.d.ts +0 -26
  95. package/dist/src/status.js +0 -74
  96. /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
  97. /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
  98. /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
  99. /package/{dist/src/index.d.ts → src/index.ts} +0 -0
@@ -0,0 +1,316 @@
1
+ import type { UIMessage } from "ai";
2
+ import { resolvePartKind, toolNameOfPart } from "../parts/kind";
3
+
4
+ const USER_STOP_REASON = "Stopped by user";
5
+
6
+ type UnknownRecord = Record<string, unknown>;
7
+
8
+ export type CamelPart = UIMessage["parts"][number];
9
+
10
+ export interface CamelMessageMetadata extends UnknownRecord {
11
+ authorDisplayName?: string;
12
+ completedAt?: number;
13
+ createdAt?: number;
14
+ error?: string;
15
+ interruptedByUser?: boolean;
16
+ messageSource?: string;
17
+ turnDurationMs?: number;
18
+ turnStartedAt?: number;
19
+ turnStatus?: string;
20
+ }
21
+
22
+ export interface CamelPartRef {
23
+ messageId: string;
24
+ partIndex: number;
25
+ part: CamelPart;
26
+ }
27
+
28
+ export interface CamelAssistantTurn {
29
+ kind: "assistant";
30
+ key: string;
31
+ message: UIMessage;
32
+ messages: readonly [UIMessage];
33
+ actionMessageId: string;
34
+ parts: readonly CamelPartRef[];
35
+ traceParts: readonly CamelPartRef[];
36
+ finalParts: readonly CamelPartRef[];
37
+ terminalState?: {
38
+ kind: "interrupted" | "error";
39
+ title: string;
40
+ body?: string;
41
+ };
42
+ stepCount: number;
43
+ copyText: string;
44
+ replyText?: string;
45
+ durationMs?: number;
46
+ completedAt?: number;
47
+ }
48
+
49
+ export type CamelStandaloneKind =
50
+ | "user"
51
+ | "notice"
52
+ | "slash-command"
53
+ | "local-command-output"
54
+ | "compact-summary";
55
+
56
+ export interface CamelStandaloneMessage {
57
+ kind: "message";
58
+ key: string;
59
+ message: UIMessage;
60
+ displayKind: CamelStandaloneKind;
61
+ }
62
+
63
+ export type CamelMessageItem = CamelAssistantTurn | CamelStandaloneMessage;
64
+
65
+ const TRACE_PART_KINDS = new Set(["tool", "parallel", "subagents"]);
66
+
67
+ function recordOf(value: unknown): UnknownRecord {
68
+ return value != null && typeof value === "object"
69
+ ? (value as UnknownRecord)
70
+ : {};
71
+ }
72
+
73
+ function nonNegativeNumber(value: unknown): number | undefined {
74
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
75
+ ? value
76
+ : undefined;
77
+ }
78
+
79
+ export function camelMetadata(message: UIMessage): CamelMessageMetadata {
80
+ return recordOf(message.metadata) as CamelMessageMetadata;
81
+ }
82
+
83
+ export const camelMessageMetadata = camelMetadata;
84
+
85
+ function isVisibleText(
86
+ part: CamelPart,
87
+ ): part is CamelPart & { type: "text"; text: string } {
88
+ return part.type === "text" && part.text.trim().length > 0;
89
+ }
90
+
91
+ function isVisibleReasoning(part: CamelPart): boolean {
92
+ return part.type === "reasoning" && part.text.trim().length > 0;
93
+ }
94
+
95
+ function standalonePartKind(part: CamelPart): CamelStandaloneKind | null {
96
+ switch (part.type) {
97
+ case "data-notice":
98
+ return "notice";
99
+ case "data-slash-command":
100
+ return "slash-command";
101
+ case "data-local-command-output":
102
+ return "local-command-output";
103
+ case "data-compact-summary":
104
+ return "compact-summary";
105
+ default:
106
+ return null;
107
+ }
108
+ }
109
+
110
+ function standaloneKind(message: UIMessage): CamelStandaloneKind | null {
111
+ for (const part of message.parts) {
112
+ const kind = standalonePartKind(part);
113
+ if (kind !== null) return kind;
114
+ }
115
+ return message.role === "user" ? "user" : null;
116
+ }
117
+
118
+ function isDirectUserMessage(message: UIMessage): boolean {
119
+ return message.role === "user" && standaloneKind(message) === "user";
120
+ }
121
+
122
+ export function camelMessageText(message: UIMessage): string {
123
+ return message.parts
124
+ .filter(isVisibleText)
125
+ .map((part) => part.text)
126
+ .join("\n");
127
+ }
128
+
129
+ export function camelCreatedAt(message: UIMessage): number | undefined {
130
+ return nonNegativeNumber(camelMetadata(message).createdAt);
131
+ }
132
+
133
+ export function camelCompletedAt(message: UIMessage): number | undefined {
134
+ return (
135
+ nonNegativeNumber(camelMetadata(message).completedAt) ??
136
+ camelCreatedAt(message)
137
+ );
138
+ }
139
+
140
+ export function formatCamelMessageTime(timestamp: number): string {
141
+ const date = new Date(timestamp);
142
+ const now = new Date();
143
+ const isToday =
144
+ date.getFullYear() === now.getFullYear() &&
145
+ date.getMonth() === now.getMonth() &&
146
+ date.getDate() === now.getDate();
147
+ return date.toLocaleString([], {
148
+ ...(isToday ? {} : { month: "short", day: "numeric" }),
149
+ hour: "numeric",
150
+ minute: "2-digit",
151
+ hour12: true,
152
+ });
153
+ }
154
+
155
+ export function formatCamelTurnDuration(milliseconds: number): string {
156
+ const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
157
+ const hours = Math.floor(totalSeconds / 3600);
158
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
159
+ const seconds = totalSeconds % 60;
160
+ return hours > 0
161
+ ? `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`
162
+ : `${minutes}:${String(seconds).padStart(2, "0")}`;
163
+ }
164
+
165
+ function isTraceWork(
166
+ part: CamelPart,
167
+ finalToolNames: readonly string[],
168
+ ): boolean {
169
+ if (isVisibleReasoning(part)) return true;
170
+ const kind = resolvePartKind(part);
171
+ if (kind === "plan") return false;
172
+ const toolName = toolNameOfPart(part);
173
+ if (toolName !== null && finalToolNames.includes(toolName)) return false;
174
+ return TRACE_PART_KINDS.has(kind ?? "");
175
+ }
176
+
177
+ function isTraceBoundary(
178
+ part: CamelPart,
179
+ finalToolNames: readonly string[],
180
+ ): boolean {
181
+ return part.type === "step-start" || isTraceWork(part, finalToolNames);
182
+ }
183
+
184
+ function terminalState(
185
+ message: UIMessage,
186
+ ): CamelAssistantTurn["terminalState"] {
187
+ const metadata = camelMetadata(message);
188
+ if (metadata.interruptedByUser === true) {
189
+ return {
190
+ kind: "interrupted",
191
+ title: USER_STOP_REASON,
192
+ };
193
+ }
194
+ if (
195
+ metadata.turnStatus === "aborted" &&
196
+ metadata.error === USER_STOP_REASON
197
+ ) return undefined;
198
+ if (metadata.turnStatus === "error" || metadata.error) {
199
+ return {
200
+ kind: "error",
201
+ title: "Error",
202
+ body: metadata.error ?? "The response failed.",
203
+ };
204
+ }
205
+ return undefined;
206
+ }
207
+
208
+ function turnDuration(message: UIMessage): number | undefined {
209
+ const metadata = camelMetadata(message);
210
+ const explicit = nonNegativeNumber(metadata.turnDurationMs);
211
+ if (explicit !== undefined) return explicit;
212
+ const startedAt = nonNegativeNumber(metadata.turnStartedAt);
213
+ const completedAt = nonNegativeNumber(metadata.completedAt);
214
+ return startedAt !== undefined && completedAt !== undefined
215
+ ? Math.max(0, completedAt - startedAt)
216
+ : undefined;
217
+ }
218
+
219
+ export function projectCamelAssistantTurn(
220
+ message: UIMessage,
221
+ finalToolNames: readonly string[] = [],
222
+ replyText?: string,
223
+ ): CamelAssistantTurn {
224
+ const parts = message.parts.map((part, partIndex) => ({
225
+ messageId: message.id,
226
+ partIndex,
227
+ part,
228
+ }));
229
+ let lastTraceBoundary = -1;
230
+ for (let index = parts.length - 1; index >= 0; index -= 1) {
231
+ if (isTraceBoundary(parts[index].part, finalToolNames)) {
232
+ lastTraceBoundary = index;
233
+ break;
234
+ }
235
+ }
236
+
237
+ const traceParts: CamelPartRef[] = [];
238
+ const finalParts: CamelPartRef[] = [];
239
+ for (const partRef of parts) {
240
+ const { part, partIndex } = partRef;
241
+ if (part.type === "step-start" || resolvePartKind(part) === "plan") {
242
+ continue;
243
+ }
244
+ if (
245
+ isTraceWork(part, finalToolNames) ||
246
+ (isVisibleText(part) && partIndex < lastTraceBoundary)
247
+ ) {
248
+ traceParts.push(partRef);
249
+ continue;
250
+ }
251
+ const kind = resolvePartKind(part);
252
+ if (isVisibleText(part) || (kind !== null && kind !== "reasoning")) {
253
+ finalParts.push(partRef);
254
+ }
255
+ }
256
+
257
+ const hasFinalNarrative = finalParts.some(
258
+ ({ part }) => isVisibleText(part) || resolvePartKind(part) === "error",
259
+ );
260
+ const terminal = terminalState(message);
261
+
262
+ return {
263
+ kind: "assistant",
264
+ key: `assistant-${message.id}`,
265
+ message,
266
+ messages: [message],
267
+ actionMessageId: message.id,
268
+ parts,
269
+ traceParts,
270
+ finalParts,
271
+ terminalState: terminal?.kind === "interrupted" || !hasFinalNarrative
272
+ ? terminal
273
+ : undefined,
274
+ stepCount: Math.max(
275
+ 1,
276
+ parts.filter(({ part }) => part.type === "step-start").length,
277
+ ),
278
+ copyText: camelMessageText(message),
279
+ replyText,
280
+ durationMs: turnDuration(message),
281
+ completedAt: camelCompletedAt(message),
282
+ };
283
+ }
284
+
285
+ export function projectCamelMessages(
286
+ messages: readonly UIMessage[],
287
+ finalToolNames: readonly string[] = [],
288
+ ): CamelMessageItem[] {
289
+ const items: CamelMessageItem[] = [];
290
+ messages.forEach((message, index) => {
291
+ if (message.role === "system") return;
292
+ const displayKind = standaloneKind(message);
293
+ if (message.role === "assistant" && displayKind === null) {
294
+ const reply = messages
295
+ .slice(index + 1)
296
+ .find(isDirectUserMessage);
297
+ items.push(
298
+ projectCamelAssistantTurn(
299
+ message,
300
+ finalToolNames,
301
+ reply ? camelMessageText(reply).trim() || undefined : undefined,
302
+ ),
303
+ );
304
+ return;
305
+ }
306
+ if (displayKind !== null) {
307
+ items.push({
308
+ kind: "message",
309
+ key: `message-${message.id}`,
310
+ message,
311
+ displayKind,
312
+ });
313
+ }
314
+ });
315
+ return items;
316
+ }
@@ -0,0 +1,81 @@
1
+ import { ChevronRightIcon } from "lucide-react";
2
+ import { useState, type ReactNode } from "react";
3
+ import { cn } from "../internal/cn";
4
+ import { formatCamelTurnDuration } from "./camel-turn";
5
+
6
+ export interface TurnSummaryBarProps {
7
+ durationMs?: number;
8
+ stepCount: number;
9
+ children: ReactNode;
10
+ showSeparator?: boolean;
11
+ }
12
+
13
+ export function TurnSummaryBar({
14
+ durationMs,
15
+ stepCount,
16
+ children,
17
+ showSeparator = true,
18
+ }: TurnSummaryBarProps) {
19
+ const [expanded, setExpanded] = useState(false);
20
+ const stepLabel = `${stepCount} step${stepCount === 1 ? "" : "s"}`;
21
+ const showDuration = durationMs !== undefined && durationMs >= 1000;
22
+ const toggleLabel = expanded ? "hide work" : "show work";
23
+
24
+ return (
25
+ <div data-camel-turn-summary="" data-state={expanded ? "open" : "closed"}>
26
+ <button
27
+ type="button"
28
+ aria-expanded={expanded}
29
+ aria-label={`${expanded ? "Hide" : "Show"} work, ${stepLabel}${
30
+ showDuration ? `, ${formatCamelTurnDuration(durationMs)}` : ""
31
+ }`}
32
+ className={cn(
33
+ "group/turn-summary flex w-full cursor-pointer items-center gap-1.5 rounded py-1 text-left",
34
+ "font-mono text-xs text-muted-foreground/60 transition-colors duration-150",
35
+ "hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/50",
36
+ )}
37
+ onClick={() => setExpanded((current) => !current)}
38
+ >
39
+ {showDuration && (
40
+ <>
41
+ <span>worked for</span>
42
+ <span className="text-muted-foreground/80">
43
+ {formatCamelTurnDuration(durationMs)}
44
+ </span>
45
+ <span className="text-muted-foreground/30">·</span>
46
+ </>
47
+ )}
48
+ <span className="text-muted-foreground/80">{stepLabel}</span>
49
+ <span className="text-muted-foreground/30">·</span>
50
+ <span>{toggleLabel}</span>
51
+ <ChevronRightIcon
52
+ className={cn(
53
+ "size-3.5 text-muted-foreground/40 transition-transform duration-250",
54
+ expanded && "rotate-90",
55
+ )}
56
+ aria-hidden="true"
57
+ />
58
+ </button>
59
+ <div
60
+ data-camel-turn-summary-content=""
61
+ data-camel-collapsible-content=""
62
+ data-state={expanded ? "open" : "closed"}
63
+ aria-hidden={!expanded}
64
+ inert={!expanded ? true : undefined}
65
+ >
66
+ {expanded && (
67
+ <div data-camel-collapsible-inner="">
68
+ <div
69
+ className="space-y-1 py-2"
70
+ data-camel-turn-trace=""
71
+ data-state="open"
72
+ >
73
+ {children}
74
+ </div>
75
+ </div>
76
+ )}
77
+ </div>
78
+ {showSeparator && <hr className="my-2 border-t border-border/40" />}
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,229 @@
1
+ import { useState, type ComponentType } from "react";
2
+ import {
3
+ ActivityIcon,
4
+ BotIcon,
5
+ ChevronUpIcon,
6
+ FileIcon,
7
+ LinkIcon,
8
+ ListIcon,
9
+ MessageSquareIcon,
10
+ WrenchIcon,
11
+ } from "lucide-react";
12
+ import { cn } from "./internal/cn";
13
+
14
+ export interface ChatSummarySource {
15
+ kind: "file" | "link";
16
+ label: string;
17
+ href?: string;
18
+ }
19
+
20
+ export interface ChatSummaryPanelProps {
21
+ sessionTitle: string;
22
+ messageCount: number;
23
+ currentGoal?: string;
24
+ agentTitle: string;
25
+ agentValue?: string;
26
+ stateLabel: string;
27
+ stateTone: "danger" | "active" | "success";
28
+ activityCount: number;
29
+ taskCount: number;
30
+ runningTaskCount: number;
31
+ completedTaskCount: number;
32
+ sources: readonly ChatSummarySource[];
33
+ defaultExpanded?: boolean;
34
+ className?: string;
35
+ }
36
+
37
+ function SummaryRow({
38
+ icon: Icon,
39
+ label,
40
+ value,
41
+ }: {
42
+ icon: ComponentType<{ className?: string }>;
43
+ label: string;
44
+ value?: string;
45
+ }) {
46
+ return (
47
+ <div className="flex w-full min-w-0 items-center gap-2 rounded-md py-1.5 text-left text-sm text-foreground">
48
+ <Icon className="size-4 shrink-0 text-muted-foreground" />
49
+ <span className="min-w-0 flex-1 truncate">{label}</span>
50
+ {value && (
51
+ <span className="shrink-0 text-xs text-muted-foreground">{value}</span>
52
+ )}
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export function ChatSummaryPanel({
58
+ sessionTitle,
59
+ messageCount,
60
+ currentGoal,
61
+ agentTitle,
62
+ agentValue,
63
+ stateLabel,
64
+ stateTone,
65
+ activityCount,
66
+ taskCount,
67
+ runningTaskCount,
68
+ completedTaskCount,
69
+ sources,
70
+ defaultExpanded = false,
71
+ className,
72
+ }: ChatSummaryPanelProps) {
73
+ const [expanded, setExpanded] = useState(defaultExpanded);
74
+ const stateColor = {
75
+ danger: "bg-destructive",
76
+ active: "bg-primary",
77
+ success: "bg-success",
78
+ }[stateTone];
79
+
80
+ if (!expanded) {
81
+ return (
82
+ <button
83
+ type="button"
84
+ className={cn(
85
+ "inline-flex h-8 shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-border bg-background/90 px-2.5 text-xs font-medium text-muted-foreground shadow-sm backdrop-blur transition-all outline-none",
86
+ "hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50",
87
+ "animate-in fade-in-0 zoom-in-95 slide-in-from-top-1 duration-150 motion-reduce:animate-none",
88
+ className,
89
+ )}
90
+ onClick={() => setExpanded(true)}
91
+ aria-expanded={false}
92
+ aria-label="展开摘要"
93
+ title="展开摘要"
94
+ data-message-panel-summary=""
95
+ data-state="collapsed"
96
+ >
97
+ <ListIcon className="size-3.5" />
98
+ 摘要
99
+ </button>
100
+ );
101
+ }
102
+
103
+ return (
104
+ <aside
105
+ className={cn(
106
+ "w-[19rem] min-w-0 max-w-full overflow-hidden rounded-2xl border border-border bg-popover shadow-card",
107
+ "animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 duration-200 motion-reduce:animate-none",
108
+ className,
109
+ )}
110
+ data-message-panel-summary=""
111
+ data-state="expanded"
112
+ >
113
+ <header className="flex h-10 items-center border-b border-border px-4">
114
+ <span className="text-sm font-medium text-muted-foreground">摘要</span>
115
+ <button
116
+ type="button"
117
+ className="group/collapse ml-auto inline-flex size-8 shrink-0 items-center justify-center rounded-md outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 [&_svg]:size-4"
118
+ onClick={() => setExpanded(false)}
119
+ aria-expanded={true}
120
+ aria-label="收起摘要"
121
+ title="收起摘要"
122
+ >
123
+ <ChevronUpIcon className="transition-transform duration-200 group-hover/collapse:-translate-y-0.5 motion-reduce:transition-none" />
124
+ </button>
125
+ </header>
126
+ <div className="max-h-[min(31rem,calc(100dvh-8rem))] overflow-y-auto">
127
+ <div className="px-4 py-3">
128
+ <section>
129
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
130
+ 会话
131
+ </p>
132
+ <SummaryRow
133
+ icon={MessageSquareIcon}
134
+ label={sessionTitle}
135
+ value={`${messageCount} 条消息`}
136
+ />
137
+ {currentGoal && (
138
+ <p className="mb-1 ml-6 line-clamp-2 text-xs leading-5 text-muted-foreground">
139
+ {currentGoal}
140
+ </p>
141
+ )}
142
+ <SummaryRow
143
+ icon={BotIcon}
144
+ label={agentTitle}
145
+ value={agentValue}
146
+ />
147
+ <div className="flex items-center gap-2 py-1.5 text-sm">
148
+ <ActivityIcon className="size-4 shrink-0 text-muted-foreground" />
149
+ <span className="min-w-0 flex-1">状态</span>
150
+ <span
151
+ className={cn(
152
+ "size-1.5 rounded-full transition-colors duration-200",
153
+ stateTone === "active" &&
154
+ "animate-pulse motion-reduce:animate-none",
155
+ stateColor,
156
+ )}
157
+ />
158
+ <span className="text-xs text-muted-foreground">{stateLabel}</span>
159
+ </div>
160
+ <SummaryRow
161
+ icon={WrenchIcon}
162
+ label={`${activityCount} 次工具活动`}
163
+ />
164
+ </section>
165
+
166
+ <section className="mt-3 border-t border-border pt-3">
167
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
168
+ 子任务
169
+ </p>
170
+ <SummaryRow
171
+ icon={BotIcon}
172
+ label={
173
+ taskCount === 0 ? "暂无子任务" : `${runningTaskCount} 个运行中`
174
+ }
175
+ value={
176
+ completedTaskCount > 0
177
+ ? `${completedTaskCount} 已完成`
178
+ : undefined
179
+ }
180
+ />
181
+ </section>
182
+
183
+ <section className="mt-3 border-t border-border pt-3">
184
+ <p className="mb-2 text-xs font-medium text-muted-foreground">
185
+ 来源
186
+ </p>
187
+ {sources.length === 0 ? (
188
+ <p className="py-1.5 text-xs text-muted-foreground">
189
+ 当前会话还没有附件或引用来源
190
+ </p>
191
+ ) : (
192
+ <div className="space-y-0.5">
193
+ {sources.map((source) => {
194
+ const Icon = source.kind === "file" ? FileIcon : LinkIcon;
195
+ const content = (
196
+ <>
197
+ <Icon className="size-4 shrink-0 text-muted-foreground" />
198
+ <span className="min-w-0 flex-1 truncate">
199
+ {source.label}
200
+ </span>
201
+ </>
202
+ );
203
+ return source.href ? (
204
+ <a
205
+ key={`${source.kind}:${source.href}`}
206
+ href={source.href}
207
+ target="_blank"
208
+ rel="noreferrer"
209
+ className="flex items-center gap-2 rounded-md py-1.5 text-sm text-foreground hover:bg-accent"
210
+ >
211
+ {content}
212
+ </a>
213
+ ) : (
214
+ <div
215
+ key={`${source.kind}:${source.label}`}
216
+ className="flex items-center gap-2 py-1.5 text-sm text-foreground"
217
+ >
218
+ {content}
219
+ </div>
220
+ );
221
+ })}
222
+ </div>
223
+ )}
224
+ </section>
225
+ </div>
226
+ </div>
227
+ </aside>
228
+ );
229
+ }