@springbrand/message-panel 0.1.3-alpha.0 → 0.1.3-alpha.10
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/cloud-os/README.md +71 -0
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +660 -0
- package/cloud-os/chat/markdown-message.tsx +78 -0
- package/cloud-os/chat/rich-blocks.tsx +787 -0
- package/cloud-os/chat/tool-presentation.ts +586 -0
- package/cloud-os/chat/tool-rows.tsx +216 -0
- package/cloud-os/chat/transcript-model.ts +780 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +686 -0
- package/cloud-os/file-view.tsx +258 -0
- package/cloud-os/index.ts +123 -0
- package/cloud-os/internal/cn.ts +6 -0
- package/cloud-os/internal/theme-context.tsx +16 -0
- package/cloud-os/layout/cloud-os-root.tsx +34 -0
- package/cloud-os/layout/cloud-os-workspace-split.tsx +228 -0
- package/cloud-os/primitives/dropdown-menu.tsx +82 -0
- package/cloud-os/primitives/tooltip.tsx +68 -0
- package/cloud-os/primitives/workshop-controls.tsx +114 -0
- package/cloud-os/styles/cloud-os.css +553 -0
- package/cloud-os/workspace/cloud-os-workspace-panel.tsx +272 -0
- package/demo/camel-chat-showcase.tsx +13 -917
- package/demo/chat-scenarios.ts +926 -0
- package/demo/cloud-os-chat-showcase.tsx +481 -0
- package/demo/index.ts +2 -0
- package/package.json +17 -5
- package/src/camel/camel-chat-messages.tsx +55 -20
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/camel/camel-turn.ts +21 -3
- package/src/chat-summary-panel.tsx +1 -1
- package/src/composer/chat-composer.tsx +2 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
- package/src/composer/composer.tsx +2 -1
- package/src/composer/index.ts +1 -0
- package/src/composer/key-rules.ts +12 -0
- package/src/message.tsx +0 -8
- package/src/parts/plan.ts +3 -2
- package/src/styles/index.css +4 -1
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// camel 是 dev-only 的展示变体(App.tsx 的 `/__camel-chat` 路由),没有接生产宿主。
|
|
2
|
+
// 它的交互卡片仍用旧的 `onAnswer(payload)` 回调,**没有**迁到 client-settled tool 的
|
|
3
|
+
// `onRespond(toolCallId, response)` —— 生产聊天面板走的是 cloud-os,已经迁完。
|
|
4
|
+
// 哪天 camel 要上生产,这里必须一起改,否则卡片点了不会把答案回写给那次工具调用。
|
|
5
|
+
|
|
1
6
|
import type { UIMessage } from "ai";
|
|
2
7
|
import {
|
|
3
8
|
ArrowDownIcon,
|
|
@@ -71,9 +76,7 @@ export interface CamelChatMessagesProps {
|
|
|
71
76
|
finalToolNames?: readonly string[];
|
|
72
77
|
isHydrating?: boolean;
|
|
73
78
|
isRecovering?: boolean;
|
|
74
|
-
|
|
75
|
-
/** Show the opt-in activity bar for recovering or server-streaming states. */
|
|
76
|
-
showActivityStatus?: boolean;
|
|
79
|
+
recoveryStatusLabel?: string;
|
|
77
80
|
hasPendingSteer?: boolean;
|
|
78
81
|
startedWithPending?: boolean;
|
|
79
82
|
error?: unknown;
|
|
@@ -163,6 +166,23 @@ export function CamelChatError({
|
|
|
163
166
|
);
|
|
164
167
|
}
|
|
165
168
|
|
|
169
|
+
export function CamelChatRecovery({
|
|
170
|
+
label = "模型响应暂时中断,正在重试…",
|
|
171
|
+
}: {
|
|
172
|
+
label?: string;
|
|
173
|
+
}) {
|
|
174
|
+
return (
|
|
175
|
+
<div
|
|
176
|
+
className="mt-4 flex items-center gap-2 rounded-lg border border-border/70 bg-muted/40 px-3 py-2 text-sm text-muted-foreground"
|
|
177
|
+
data-camel-chat-recovery=""
|
|
178
|
+
role="status"
|
|
179
|
+
>
|
|
180
|
+
<Loader2Icon className="size-4 shrink-0 animate-spin" aria-hidden="true" />
|
|
181
|
+
<span>{label}</span>
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
166
186
|
export function CamelApprovalPrompts({
|
|
167
187
|
approvals,
|
|
168
188
|
error,
|
|
@@ -403,16 +423,21 @@ function CamelToolRow({
|
|
|
403
423
|
onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
|
|
404
424
|
getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
|
|
405
425
|
}) {
|
|
406
|
-
const [open, setOpen] = useState(false);
|
|
407
426
|
const approval = getToolApproval?.(part) ?? toolApprovalOf(part);
|
|
408
427
|
const pendingApproval = approval && approval.approved === undefined;
|
|
409
428
|
const presentation = getCamelToolPresentation(part, isActive);
|
|
429
|
+
const autoOpen = presentation.running && (
|
|
430
|
+
presentation.isSubAgent ||
|
|
431
|
+
presentation.kind === "write" ||
|
|
432
|
+
presentation.kind === "edit"
|
|
433
|
+
);
|
|
434
|
+
const [open, setOpen] = useState(autoOpen);
|
|
410
435
|
const hasDetails = children != null || presentation.hasDetails;
|
|
411
436
|
const title = presentation.summary;
|
|
412
437
|
|
|
413
438
|
useEffect(() => {
|
|
414
|
-
if (
|
|
415
|
-
}, [
|
|
439
|
+
if (autoOpen) setOpen(true);
|
|
440
|
+
}, [autoOpen]);
|
|
416
441
|
|
|
417
442
|
return (
|
|
418
443
|
<div data-part-type={normalizedPartType(part)}>
|
|
@@ -1145,6 +1170,7 @@ function RenderAssistantPart({
|
|
|
1145
1170
|
function AssistantTurn({
|
|
1146
1171
|
turn,
|
|
1147
1172
|
isActive,
|
|
1173
|
+
keepTraceExpanded,
|
|
1148
1174
|
hasPendingSteer,
|
|
1149
1175
|
hasPendingApproval,
|
|
1150
1176
|
onFork,
|
|
@@ -1156,6 +1182,7 @@ function AssistantTurn({
|
|
|
1156
1182
|
}: {
|
|
1157
1183
|
turn: CamelAssistantTurn;
|
|
1158
1184
|
isActive: boolean;
|
|
1185
|
+
keepTraceExpanded: boolean;
|
|
1159
1186
|
hasPendingSteer: boolean;
|
|
1160
1187
|
hasPendingApproval: boolean;
|
|
1161
1188
|
onFork?: (messageId: string) => void | Promise<void>;
|
|
@@ -1179,7 +1206,10 @@ function AssistantTurn({
|
|
|
1179
1206
|
/>
|
|
1180
1207
|
);
|
|
1181
1208
|
const showWorkInPlace =
|
|
1182
|
-
isActive ||
|
|
1209
|
+
isActive ||
|
|
1210
|
+
keepTraceExpanded ||
|
|
1211
|
+
hasPendingApproval ||
|
|
1212
|
+
turn.terminalState !== undefined;
|
|
1183
1213
|
const showSummary = !showWorkInPlace && turn.traceParts.length > 0;
|
|
1184
1214
|
const completedAt = turn.completedAt ?? camelCompletedAt(turn.message);
|
|
1185
1215
|
const messageTime =
|
|
@@ -1189,6 +1219,11 @@ function AssistantTurn({
|
|
|
1189
1219
|
const thinkingActivity = deriveThinkingActivity(
|
|
1190
1220
|
turn.traceParts.at(-1)?.part ?? turn.finalParts.at(-1)?.part,
|
|
1191
1221
|
);
|
|
1222
|
+
const hasRunningTool = turn.traceParts.some(
|
|
1223
|
+
({ part }) =>
|
|
1224
|
+
toolNameOfPart(part) !== null &&
|
|
1225
|
+
getCamelToolPresentation(part, isActive).running,
|
|
1226
|
+
);
|
|
1192
1227
|
const activeActivity = hasPendingSteer
|
|
1193
1228
|
? pendingSteerActivity
|
|
1194
1229
|
: thinkingActivity;
|
|
@@ -1269,7 +1304,7 @@ function AssistantTurn({
|
|
|
1269
1304
|
{turn.copyText && <ClipboardAction text={turn.copyText} />}
|
|
1270
1305
|
</div>
|
|
1271
1306
|
)}
|
|
1272
|
-
{isActive && turn.finalParts.length === 0 && (
|
|
1307
|
+
{isActive && turn.finalParts.length === 0 && !hasRunningTool && (
|
|
1273
1308
|
<ThinkingIndicator {...activeActivity} />
|
|
1274
1309
|
)}
|
|
1275
1310
|
</div>
|
|
@@ -1283,8 +1318,7 @@ export function CamelChatMessages({
|
|
|
1283
1318
|
finalToolNames,
|
|
1284
1319
|
isHydrating = false,
|
|
1285
1320
|
isRecovering = false,
|
|
1286
|
-
|
|
1287
|
-
showActivityStatus = false,
|
|
1321
|
+
recoveryStatusLabel,
|
|
1288
1322
|
hasPendingSteer = false,
|
|
1289
1323
|
startedWithPending = false,
|
|
1290
1324
|
error,
|
|
@@ -1316,6 +1350,10 @@ export function CamelChatMessages({
|
|
|
1316
1350
|
isActive && lastItem?.kind === "assistant"
|
|
1317
1351
|
? lastItem.actionMessageId
|
|
1318
1352
|
: null;
|
|
1353
|
+
const recoveringAssistantMessageId =
|
|
1354
|
+
isRecovering && lastItem?.kind === "assistant"
|
|
1355
|
+
? lastItem.actionMessageId
|
|
1356
|
+
: null;
|
|
1319
1357
|
|
|
1320
1358
|
const syncScrollButton = useCallback(() => {
|
|
1321
1359
|
const element = scrollRef.current;
|
|
@@ -1329,7 +1367,7 @@ export function CamelChatMessages({
|
|
|
1329
1367
|
const element = scrollRef.current;
|
|
1330
1368
|
if (!element || showScrollButton) return;
|
|
1331
1369
|
element.scrollTo({ top: element.scrollHeight });
|
|
1332
|
-
}, [messages, status, showScrollButton]);
|
|
1370
|
+
}, [isRecovering, messages, status, showScrollButton]);
|
|
1333
1371
|
|
|
1334
1372
|
return (
|
|
1335
1373
|
<div
|
|
@@ -1339,15 +1377,6 @@ export function CamelChatMessages({
|
|
|
1339
1377
|
data-status={status}
|
|
1340
1378
|
aria-busy={isActive || isHydrating}
|
|
1341
1379
|
>
|
|
1342
|
-
{showActivityStatus && (isRecovering || isServerStreaming) && (
|
|
1343
|
-
<div
|
|
1344
|
-
className="absolute inset-x-0 top-0 z-10 flex h-7 items-center justify-center gap-2 bg-background/85 text-xs text-muted-foreground backdrop-blur-sm"
|
|
1345
|
-
role="status"
|
|
1346
|
-
>
|
|
1347
|
-
<span className="size-1.5 animate-pulse rounded-full bg-blue-500" />
|
|
1348
|
-
{isRecovering ? "Recovering…" : "Running…"}
|
|
1349
|
-
</div>
|
|
1350
|
-
)}
|
|
1351
1380
|
<div
|
|
1352
1381
|
ref={scrollRef}
|
|
1353
1382
|
onScroll={syncScrollButton}
|
|
@@ -1368,6 +1397,9 @@ export function CamelChatMessages({
|
|
|
1368
1397
|
key={item.key}
|
|
1369
1398
|
turn={item}
|
|
1370
1399
|
isActive={item.actionMessageId === activeAssistantMessageId}
|
|
1400
|
+
keepTraceExpanded={
|
|
1401
|
+
item.actionMessageId === recoveringAssistantMessageId
|
|
1402
|
+
}
|
|
1371
1403
|
hasPendingSteer={hasPendingSteer}
|
|
1372
1404
|
hasPendingApproval={item.parts.some((partRef) => {
|
|
1373
1405
|
const approval =
|
|
@@ -1386,6 +1418,9 @@ export function CamelChatMessages({
|
|
|
1386
1418
|
<StandaloneMessage key={item.key} item={item} />
|
|
1387
1419
|
),
|
|
1388
1420
|
)}
|
|
1421
|
+
{isRecovering && (
|
|
1422
|
+
<CamelChatRecovery label={recoveryStatusLabel} />
|
|
1423
|
+
)}
|
|
1389
1424
|
{error != null && <CamelChatError error={error} onRetry={onRetry} />}
|
|
1390
1425
|
{isActive && activeAssistantMessageId === null && (
|
|
1391
1426
|
<ThinkingIndicator
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
type ReactNode,
|
|
40
40
|
} from "react";
|
|
41
41
|
import { ComposerTriggerPopover } from "../composer/composer-trigger-popover";
|
|
42
|
+
import { isComposingKeyEvent } from "../composer/key-rules";
|
|
42
43
|
import type { ComposerCommand } from "../composer/types";
|
|
43
44
|
import { cn } from "../internal/cn";
|
|
44
45
|
|
|
@@ -345,7 +346,7 @@ function CamelPromptInputContent({
|
|
|
345
346
|
if (
|
|
346
347
|
event.key === "Enter" &&
|
|
347
348
|
!event.shiftKey &&
|
|
348
|
-
!event.nativeEvent
|
|
349
|
+
!isComposingKeyEvent(event.nativeEvent)
|
|
349
350
|
) {
|
|
350
351
|
event.preventDefault();
|
|
351
352
|
submit();
|
package/src/camel/camel-turn.ts
CHANGED
|
@@ -169,11 +169,20 @@ function isTraceWork(
|
|
|
169
169
|
if (isVisibleReasoning(part)) return true;
|
|
170
170
|
const kind = resolvePartKind(part);
|
|
171
171
|
if (kind === "plan") return false;
|
|
172
|
-
|
|
173
|
-
if (toolName !== null && finalToolNames.includes(toolName)) return false;
|
|
172
|
+
if (isFinalOutputTool(part, finalToolNames)) return false;
|
|
174
173
|
return TRACE_PART_KINDS.has(kind ?? "");
|
|
175
174
|
}
|
|
176
175
|
|
|
176
|
+
function isFinalOutputTool(
|
|
177
|
+
part: CamelPart,
|
|
178
|
+
finalToolNames: readonly string[],
|
|
179
|
+
): boolean {
|
|
180
|
+
const toolName = toolNameOfPart(part);
|
|
181
|
+
return toolName !== null &&
|
|
182
|
+
(finalToolNames.includes(toolName) ||
|
|
183
|
+
resolvePartKind(part) === "suggestions");
|
|
184
|
+
}
|
|
185
|
+
|
|
177
186
|
function isTraceBoundary(
|
|
178
187
|
part: CamelPart,
|
|
179
188
|
finalToolNames: readonly string[],
|
|
@@ -226,8 +235,17 @@ export function projectCamelAssistantTurn(
|
|
|
226
235
|
partIndex,
|
|
227
236
|
part,
|
|
228
237
|
}));
|
|
229
|
-
let
|
|
238
|
+
let finalOutputBoundary = parts.length;
|
|
230
239
|
for (let index = parts.length - 1; index >= 0; index -= 1) {
|
|
240
|
+
if (isFinalOutputTool(parts[index].part, finalToolNames)) {
|
|
241
|
+
finalOutputBoundary = index;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
let lastTraceBoundary = -1;
|
|
246
|
+
// Provider cleanup steps after final output must not pull the answer back
|
|
247
|
+
// into the collapsible work trace.
|
|
248
|
+
for (let index = finalOutputBoundary - 1; index >= 0; index -= 1) {
|
|
231
249
|
if (isTraceBoundary(parts[index].part, finalToolNames)) {
|
|
232
250
|
lastTraceBoundary = index;
|
|
233
251
|
break;
|
|
@@ -103,7 +103,7 @@ export function ChatSummaryPanel({
|
|
|
103
103
|
return (
|
|
104
104
|
<aside
|
|
105
105
|
className={cn(
|
|
106
|
-
"w-[19rem] min-w-0 max-w-
|
|
106
|
+
"absolute right-3 top-11 z-30 w-[19rem] min-w-0 max-w-[calc(100%-1.5rem)] overflow-hidden rounded-2xl border border-border bg-popover shadow-card",
|
|
107
107
|
"animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 duration-200 motion-reduce:animate-none",
|
|
108
108
|
className,
|
|
109
109
|
)}
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
type KeyboardEvent,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { cn } from "../internal/cn";
|
|
13
|
+
import { isComposingKeyEvent } from "./key-rules";
|
|
13
14
|
import { Composer } from "./composer";
|
|
14
15
|
import type {
|
|
15
16
|
ChatComposerProps,
|
|
@@ -152,7 +153,7 @@ function ChatComposerInner<TAttachment>(
|
|
|
152
153
|
if (
|
|
153
154
|
(event.key === "Enter" &&
|
|
154
155
|
!event.shiftKey &&
|
|
155
|
-
!event.nativeEvent
|
|
156
|
+
!isComposingKeyEvent(event.nativeEvent)) ||
|
|
156
157
|
event.key === "Tab"
|
|
157
158
|
) {
|
|
158
159
|
const command = filtered[active] ?? filtered[0];
|
|
@@ -156,7 +156,7 @@ function Items({
|
|
|
156
156
|
{item.label}
|
|
157
157
|
</span>
|
|
158
158
|
{item.description && (
|
|
159
|
-
<span className="ms-5.5 text-xs leading-tight text-muted-foreground">
|
|
159
|
+
<span className="ms-5.5 w-full truncate text-xs leading-tight text-muted-foreground">
|
|
160
160
|
{item.description}
|
|
161
161
|
</span>
|
|
162
162
|
)}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
Attachments,
|
|
27
27
|
} from "./composer-attachments";
|
|
28
28
|
import { useComposerDrop } from "./drop-controller";
|
|
29
|
+
import { isComposingKeyEvent } from "./key-rules";
|
|
29
30
|
import type {
|
|
30
31
|
ComposerAttachmentView,
|
|
31
32
|
ComposerProps,
|
|
@@ -247,7 +248,7 @@ function ComposerInner<TAttachment>(
|
|
|
247
248
|
if (
|
|
248
249
|
event.key === "Enter" &&
|
|
249
250
|
!event.shiftKey &&
|
|
250
|
-
!event.nativeEvent
|
|
251
|
+
!isComposingKeyEvent(event.nativeEvent)
|
|
251
252
|
) {
|
|
252
253
|
event.preventDefault();
|
|
253
254
|
if (canSend) void submit();
|
package/src/composer/index.ts
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether Enter belongs to an input method rather than to the Composer.
|
|
3
|
+
*
|
|
4
|
+
* Both signals are honoured on their own: some Chinese input methods report only the
|
|
5
|
+
* legacy `keyCode === 229`, and either one alone means a word is still being composed.
|
|
6
|
+
*/
|
|
7
|
+
export function isComposingKeyEvent(event: {
|
|
8
|
+
isComposing?: boolean;
|
|
9
|
+
keyCode?: number;
|
|
10
|
+
}): boolean {
|
|
11
|
+
return event.isComposing === true || event.keyCode === 229;
|
|
12
|
+
}
|
package/src/message.tsx
CHANGED
|
@@ -85,13 +85,6 @@ export const MessageAction = forwardRef<HTMLButtonElement, MessageActionProps>(
|
|
|
85
85
|
MessageAction.displayName = "MessageAction";
|
|
86
86
|
|
|
87
87
|
const streamdownPlugins = { cjk, code, math, mermaid };
|
|
88
|
-
const streamAnimation = {
|
|
89
|
-
animation: "fadeIn",
|
|
90
|
-
sep: "word",
|
|
91
|
-
duration: 260,
|
|
92
|
-
stagger: 14,
|
|
93
|
-
} as const;
|
|
94
|
-
|
|
95
88
|
export type MessageResponseProps = StreamdownProps & {
|
|
96
89
|
streaming?: boolean;
|
|
97
90
|
resolveUrl?: MessageUrlResolver;
|
|
@@ -146,7 +139,6 @@ export const MessageResponse = memo(
|
|
|
146
139
|
? {
|
|
147
140
|
mode: "streaming" as const,
|
|
148
141
|
isAnimating: true,
|
|
149
|
-
animated: streamAnimation,
|
|
150
142
|
}
|
|
151
143
|
: { mode: "static" as const })}
|
|
152
144
|
{...props}
|
package/src/parts/plan.ts
CHANGED
|
@@ -26,8 +26,9 @@ export function planStepsOfPart(part: unknown): PlanStep[] {
|
|
|
26
26
|
const value = recordOf(step);
|
|
27
27
|
const text = typeof value.text === "string" ? value.text.trim() : "";
|
|
28
28
|
if (!text) return [];
|
|
29
|
-
const status =
|
|
30
|
-
|
|
29
|
+
const status = value.status === "completed"
|
|
30
|
+
? "done"
|
|
31
|
+
: value.status === "in_progress" || value.status === "done"
|
|
31
32
|
? value.status
|
|
32
33
|
: "pending";
|
|
33
34
|
return [{ text, status }];
|
package/src/styles/index.css
CHANGED
|
@@ -2128,7 +2128,10 @@
|
|
|
2128
2128
|
min-height: 120px;
|
|
2129
2129
|
gap: 0;
|
|
2130
2130
|
border-radius: 16px;
|
|
2131
|
-
|
|
2131
|
+
/* 这里曾写死 #1c1c1a,于是 light 档下输入框还是近黑的。
|
|
2132
|
+
表面色一律走 token:--mp-card → --card(light #fff / dark #1c1c1c)。
|
|
2133
|
+
本节其余几个写死色是状态/图标强调色(琥珀、蓝),两档背景上都读得出来,不在此列。 */
|
|
2134
|
+
background: var(--mp-card);
|
|
2132
2135
|
box-shadow: none;
|
|
2133
2136
|
}
|
|
2134
2137
|
|