@springbrand/message-panel 0.1.3-alpha.1 → 0.1.3-alpha.11

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 (43) hide show
  1. package/cloud-os/README.md +71 -0
  2. package/cloud-os/capability-chip.tsx +35 -0
  3. package/cloud-os/chat/activity-indicator.tsx +29 -0
  4. package/cloud-os/chat/cloud-os-chat-messages.tsx +663 -0
  5. package/cloud-os/chat/markdown-message.tsx +78 -0
  6. package/cloud-os/chat/rich-blocks.tsx +944 -0
  7. package/cloud-os/chat/tool-presentation.ts +602 -0
  8. package/cloud-os/chat/tool-rows.tsx +216 -0
  9. package/cloud-os/chat/transcript-model.ts +780 -0
  10. package/cloud-os/composer/cloud-os-chat-input.tsx +686 -0
  11. package/cloud-os/file-view.tsx +258 -0
  12. package/cloud-os/index.ts +123 -0
  13. package/cloud-os/internal/cn.ts +6 -0
  14. package/cloud-os/internal/theme-context.tsx +16 -0
  15. package/cloud-os/layout/cloud-os-root.tsx +34 -0
  16. package/cloud-os/layout/cloud-os-workspace-split.tsx +228 -0
  17. package/cloud-os/primitives/dropdown-menu.tsx +82 -0
  18. package/cloud-os/primitives/tooltip.tsx +68 -0
  19. package/cloud-os/primitives/workshop-controls.tsx +114 -0
  20. package/cloud-os/styles/cloud-os.css +582 -0
  21. package/cloud-os/workspace/cloud-os-workspace-panel.tsx +272 -0
  22. package/demo/camel-chat-showcase.tsx +25 -930
  23. package/demo/chat-scenarios.ts +943 -0
  24. package/demo/cloud-os-chat-showcase.tsx +483 -0
  25. package/demo/fixtures.ts +4 -5
  26. package/demo/index.ts +2 -0
  27. package/demo/message-panel-gallery.tsx +16 -2
  28. package/package.json +17 -5
  29. package/src/camel/camel-chat-messages.tsx +118 -95
  30. package/src/camel/camel-prompt-input.tsx +2 -1
  31. package/src/camel/camel-tool-presentation.tsx +17 -13
  32. package/src/camel/camel-turn.ts +1 -17
  33. package/src/chat-summary-panel.tsx +1 -1
  34. package/src/composer/chat-composer.tsx +2 -1
  35. package/src/composer/composer-trigger-popover.tsx +1 -1
  36. package/src/composer/composer.tsx +2 -1
  37. package/src/composer/index.ts +1 -0
  38. package/src/composer/key-rules.ts +12 -0
  39. package/src/contracts.ts +0 -2
  40. package/src/message-panel.tsx +0 -23
  41. package/src/message.tsx +0 -8
  42. package/src/parts/index.tsx +102 -63
  43. 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,10 +76,7 @@ export interface CamelChatMessagesProps {
71
76
  finalToolNames?: readonly string[];
72
77
  isHydrating?: boolean;
73
78
  isRecovering?: boolean;
74
- isServerStreaming?: boolean;
75
- /** Show the opt-in activity bar for recovering or server-streaming states. */
76
- showActivityStatus?: boolean;
77
- activityStatusLabel?: string;
79
+ recoveryStatusLabel?: string;
78
80
  hasPendingSteer?: boolean;
79
81
  startedWithPending?: boolean;
80
82
  error?: unknown;
@@ -164,6 +166,23 @@ export function CamelChatError({
164
166
  );
165
167
  }
166
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
+
167
186
  export function CamelApprovalPrompts({
168
187
  approvals,
169
188
  error,
@@ -649,53 +668,30 @@ function CamelSchedule({
649
668
 
650
669
  function CamelAskUser({
651
670
  part,
652
- submittedAnswer,
653
671
  onAnswer,
654
672
  }: {
655
673
  part: unknown;
656
- submittedAnswer?: string;
657
674
  onAnswer: (payload: unknown) => void;
658
675
  }) {
659
676
  const record = recordOf(part);
660
677
  const data = dataOf(part);
661
- const output = record.output;
662
678
  const questions = Array.isArray(data.questions)
663
679
  ? data.questions.map(recordOf)
664
- : typeof data.question === "string"
665
- ? [{
666
- prompt: data.question,
667
- kind: data.multiSelect === true ? "multi" : "single",
668
- options: Array.isArray(data.options)
669
- ? data.options.filter(
670
- (option): option is string => typeof option === "string",
671
- )
672
- : [],
673
- }]
674
- : [];
675
- const [selections, setSelections] = useState<string[][]>(() =>
676
- questions.map(() => []),
677
- );
678
- const [text, setText] = useState("");
679
- const [submitted, setSubmitted] = useState(false);
680
- const outputRecord = recordOf(output);
681
- const externalAnswer =
682
- submittedAnswer?.trim() ||
683
- (typeof output === "string"
684
- ? output.trim()
685
- : typeof outputRecord.text === "string"
686
- ? outputRecord.text.trim()
687
- : "");
688
- const externalSelections = new Set(
689
- externalAnswer
690
- .split("、")
691
- .map((value) => value.trim())
692
- .filter(Boolean),
680
+ : [];
681
+ const output = recordOf(record.output);
682
+ const settledAnswers = Array.isArray(output.answers)
683
+ ? output.answers.map(recordOf)
684
+ : [];
685
+ const [answers, setAnswers] = useState<Array<{
686
+ selections: string[];
687
+ text: string;
688
+ }>>([]);
689
+ const answerAt = (index: number) =>
690
+ answers[index] ?? { selections: [], text: "" };
691
+ const resolvedSubmitted = settledAnswers.length > 0;
692
+ const valid = questions.length > 0 && questions.every((_, index) =>
693
+ answerAt(index).selections.length > 0 || answerAt(index).text.trim()
693
694
  );
694
- const resolvedSubmitted = submitted || Boolean(externalAnswer);
695
- const valid = questions.every((question, index) => {
696
- const options = Array.isArray(question.options) ? question.options : [];
697
- return options.length === 0 || (selections[index]?.length ?? 0) > 0;
698
- });
699
695
 
700
696
  return (
701
697
  <section
@@ -704,21 +700,28 @@ function CamelAskUser({
704
700
  data-state={resolvedSubmitted ? "submitted" : "idle"}
705
701
  >
706
702
  {questions.map((question, questionIndex) => {
707
- const prompt = String(question.prompt ?? "Choose an option");
703
+ const prompt = String(question.question ?? "Choose an option");
708
704
  const options = Array.isArray(question.options)
709
705
  ? question.options.filter(
710
706
  (option): option is string => typeof option === "string",
711
707
  )
712
708
  : [];
713
- const multi = question.kind === "multi";
709
+ const multi = question.multiSelect === true;
710
+ const allowCustom = question.allowCustom === true || options.length === 0;
711
+ const settledAnswer = settledAnswers[questionIndex];
712
+ const settledSelections = Array.isArray(settledAnswer?.selections)
713
+ ? settledAnswer.selections.filter(
714
+ (value): value is string => typeof value === "string",
715
+ )
716
+ : [];
714
717
  return (
715
718
  <div className="space-y-2" key={`${prompt}:${questionIndex}`}>
716
719
  <p className="text-sm text-foreground">{prompt}</p>
717
720
  <div className="flex flex-wrap gap-2">
718
721
  {options.map((option) => {
719
- const selected = externalAnswer
720
- ? externalSelections.has(option)
721
- : selections[questionIndex]?.includes(option);
722
+ const selected = settledAnswer
723
+ ? settledSelections.includes(option)
724
+ : answerAt(questionIndex).selections.includes(option);
722
725
  return (
723
726
  <Button
724
727
  key={option}
@@ -727,15 +730,21 @@ function CamelAskUser({
727
730
  variant={selected ? "default" : "outline"}
728
731
  disabled={resolvedSubmitted}
729
732
  onClick={() =>
730
- setSelections((current) =>
731
- current.map((values, index) => {
732
- if (index !== questionIndex) return values;
733
- if (!multi) return [option];
734
- return values.includes(option)
735
- ? values.filter((value) => value !== option)
736
- : [...values, option];
737
- }),
738
- )
733
+ setAnswers((current) => {
734
+ const next = questions.map((_, index) =>
735
+ current[index] ?? { selections: [], text: "" }
736
+ );
737
+ const answer = next[questionIndex]!;
738
+ next[questionIndex] = {
739
+ ...answer,
740
+ selections: !multi
741
+ ? [option]
742
+ : answer.selections.includes(option)
743
+ ? answer.selections.filter((value) => value !== option)
744
+ : [...answer.selections, option],
745
+ };
746
+ return next;
747
+ })
739
748
  }
740
749
  >
741
750
  {option}
@@ -743,34 +752,54 @@ function CamelAskUser({
743
752
  );
744
753
  })}
745
754
  </div>
755
+ {!resolvedSubmitted && allowCustom && (
756
+ <input
757
+ value={answerAt(questionIndex).text}
758
+ maxLength={2_000}
759
+ aria-label={`Custom answer: ${prompt}`}
760
+ onChange={(event) =>
761
+ setAnswers((current) => {
762
+ const next = questions.map((_, index) =>
763
+ current[index] ?? { selections: [], text: "" }
764
+ );
765
+ next[questionIndex] = {
766
+ ...next[questionIndex]!,
767
+ text: event.currentTarget.value,
768
+ };
769
+ return next;
770
+ })}
771
+ placeholder={options.length > 0 ? "Other" : "Type your answer"}
772
+ className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring focus:ring-2 focus:ring-ring/30"
773
+ />
774
+ )}
775
+ {settledAnswer && (
776
+ <p className="text-xs text-muted-foreground">
777
+ Answered: {[
778
+ ...settledSelections,
779
+ ...(typeof settledAnswer.text === "string" && settledAnswer.text.trim()
780
+ ? [settledAnswer.text.trim()]
781
+ : []),
782
+ ].join(" / ")}
783
+ </p>
784
+ )}
746
785
  </div>
747
786
  );
748
787
  })}
749
- {typeof data.freeTextPlaceholder === "string" && (
750
- <input
751
- value={text}
752
- disabled={resolvedSubmitted}
753
- onChange={(event) => setText(event.currentTarget.value)}
754
- placeholder={data.freeTextPlaceholder}
755
- className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm outline-none focus:border-ring focus:ring-2 focus:ring-ring/30"
756
- />
757
- )}
758
788
  <Button
759
789
  type="button"
760
790
  size="sm"
761
791
  disabled={resolvedSubmitted || !valid}
762
792
  onClick={() => {
763
- onAnswer({ selections, text });
764
- setSubmitted(true);
793
+ onAnswer({
794
+ answers: questions.map((_, index) => ({
795
+ selections: answerAt(index).selections,
796
+ text: answerAt(index).text.trim(),
797
+ })),
798
+ });
765
799
  }}
766
800
  >
767
801
  {resolvedSubmitted ? "Submitted" : "Submit"}
768
802
  </Button>
769
- {externalAnswer && (
770
- <p className="text-xs text-muted-foreground">
771
- 已回答:{externalAnswer}
772
- </p>
773
- )}
774
803
  </section>
775
804
  );
776
805
  }
@@ -781,7 +810,6 @@ function CamelRichPart({
781
810
  onApprove,
782
811
  onSuggestion,
783
812
  onAnswer,
784
- replyText,
785
813
  getToolApproval,
786
814
  }: {
787
815
  part: unknown;
@@ -789,7 +817,6 @@ function CamelRichPart({
789
817
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
790
818
  onSuggestion: (text: string) => void;
791
819
  onAnswer: (payload: unknown) => void;
792
- replyText?: string;
793
820
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
794
821
  }) {
795
822
  const kind = normalizedPartType(part);
@@ -805,7 +832,6 @@ function CamelRichPart({
805
832
  return (
806
833
  <CamelAskUser
807
834
  part={part}
808
- submittedAnswer={replyText}
809
835
  onAnswer={onAnswer}
810
836
  />
811
837
  );
@@ -905,7 +931,6 @@ function AssistantPart({
905
931
  onApprove,
906
932
  onSuggestion,
907
933
  onAnswer,
908
- replyText,
909
934
  getToolApproval,
910
935
  resolveUrl,
911
936
  }: {
@@ -914,7 +939,6 @@ function AssistantPart({
914
939
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
915
940
  onSuggestion: (text: string) => void;
916
941
  onAnswer: (payload: unknown) => void;
917
- replyText?: string;
918
942
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
919
943
  resolveUrl?: MessageUrlResolver;
920
944
  }) {
@@ -941,7 +965,6 @@ function AssistantPart({
941
965
  onApprove={onApprove}
942
966
  onSuggestion={onSuggestion}
943
967
  onAnswer={onAnswer}
944
- replyText={replyText}
945
968
  getToolApproval={getToolApproval}
946
969
  />
947
970
  );
@@ -1118,7 +1141,6 @@ function RenderAssistantPart({
1118
1141
  onApprove,
1119
1142
  onSuggestion,
1120
1143
  onAnswer,
1121
- replyText,
1122
1144
  getToolApproval,
1123
1145
  resolveUrl,
1124
1146
  }: {
@@ -1127,7 +1149,6 @@ function RenderAssistantPart({
1127
1149
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
1128
1150
  onSuggestion: (text: string) => void;
1129
1151
  onAnswer: (payload: unknown) => void;
1130
- replyText?: string;
1131
1152
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
1132
1153
  resolveUrl?: MessageUrlResolver;
1133
1154
  }) {
@@ -1141,7 +1162,6 @@ function RenderAssistantPart({
1141
1162
  onApprove={onApprove}
1142
1163
  onSuggestion={onSuggestion}
1143
1164
  onAnswer={onAnswer}
1144
- replyText={replyText}
1145
1165
  getToolApproval={getToolApproval}
1146
1166
  resolveUrl={resolveUrl}
1147
1167
  />
@@ -1151,6 +1171,7 @@ function RenderAssistantPart({
1151
1171
  function AssistantTurn({
1152
1172
  turn,
1153
1173
  isActive,
1174
+ keepTraceExpanded,
1154
1175
  hasPendingSteer,
1155
1176
  hasPendingApproval,
1156
1177
  onFork,
@@ -1162,6 +1183,7 @@ function AssistantTurn({
1162
1183
  }: {
1163
1184
  turn: CamelAssistantTurn;
1164
1185
  isActive: boolean;
1186
+ keepTraceExpanded: boolean;
1165
1187
  hasPendingSteer: boolean;
1166
1188
  hasPendingApproval: boolean;
1167
1189
  onFork?: (messageId: string) => void | Promise<void>;
@@ -1179,13 +1201,15 @@ function AssistantTurn({
1179
1201
  onApprove={onApprove}
1180
1202
  onSuggestion={onSuggestion}
1181
1203
  onAnswer={onAnswer}
1182
- replyText={turn.replyText}
1183
1204
  getToolApproval={getToolApproval}
1184
1205
  resolveUrl={resolveUrl}
1185
1206
  />
1186
1207
  );
1187
1208
  const showWorkInPlace =
1188
- isActive || hasPendingApproval || turn.terminalState !== undefined;
1209
+ isActive ||
1210
+ keepTraceExpanded ||
1211
+ hasPendingApproval ||
1212
+ turn.terminalState !== undefined;
1189
1213
  const showSummary = !showWorkInPlace && turn.traceParts.length > 0;
1190
1214
  const completedAt = turn.completedAt ?? camelCompletedAt(turn.message);
1191
1215
  const messageTime =
@@ -1294,9 +1318,7 @@ export function CamelChatMessages({
1294
1318
  finalToolNames,
1295
1319
  isHydrating = false,
1296
1320
  isRecovering = false,
1297
- isServerStreaming = false,
1298
- showActivityStatus = false,
1299
- activityStatusLabel,
1321
+ recoveryStatusLabel,
1300
1322
  hasPendingSteer = false,
1301
1323
  startedWithPending = false,
1302
1324
  error,
@@ -1328,6 +1350,10 @@ export function CamelChatMessages({
1328
1350
  isActive && lastItem?.kind === "assistant"
1329
1351
  ? lastItem.actionMessageId
1330
1352
  : null;
1353
+ const recoveringAssistantMessageId =
1354
+ isRecovering && lastItem?.kind === "assistant"
1355
+ ? lastItem.actionMessageId
1356
+ : null;
1331
1357
 
1332
1358
  const syncScrollButton = useCallback(() => {
1333
1359
  const element = scrollRef.current;
@@ -1341,7 +1367,7 @@ export function CamelChatMessages({
1341
1367
  const element = scrollRef.current;
1342
1368
  if (!element || showScrollButton) return;
1343
1369
  element.scrollTo({ top: element.scrollHeight });
1344
- }, [messages, status, showScrollButton]);
1370
+ }, [isRecovering, messages, status, showScrollButton]);
1345
1371
 
1346
1372
  return (
1347
1373
  <div
@@ -1351,15 +1377,6 @@ export function CamelChatMessages({
1351
1377
  data-status={status}
1352
1378
  aria-busy={isActive || isHydrating}
1353
1379
  >
1354
- {showActivityStatus && (isRecovering || isServerStreaming) && (
1355
- <div
1356
- 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"
1357
- role="status"
1358
- >
1359
- <span className="size-1.5 animate-pulse rounded-full bg-blue-500" />
1360
- {activityStatusLabel ?? (isRecovering ? "Recovering…" : "Running…")}
1361
- </div>
1362
- )}
1363
1380
  <div
1364
1381
  ref={scrollRef}
1365
1382
  onScroll={syncScrollButton}
@@ -1380,6 +1397,9 @@ export function CamelChatMessages({
1380
1397
  key={item.key}
1381
1398
  turn={item}
1382
1399
  isActive={item.actionMessageId === activeAssistantMessageId}
1400
+ keepTraceExpanded={
1401
+ item.actionMessageId === recoveringAssistantMessageId
1402
+ }
1383
1403
  hasPendingSteer={hasPendingSteer}
1384
1404
  hasPendingApproval={item.parts.some((partRef) => {
1385
1405
  const approval =
@@ -1398,6 +1418,9 @@ export function CamelChatMessages({
1398
1418
  <StandaloneMessage key={item.key} item={item} />
1399
1419
  ),
1400
1420
  )}
1421
+ {isRecovering && (
1422
+ <CamelChatRecovery label={recoveryStatusLabel} />
1423
+ )}
1401
1424
  {error != null && <CamelChatError error={error} onRetry={onRetry} />}
1402
1425
  {isActive && activeAssistantMessageId === null && (
1403
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.isComposing
349
+ !isComposingKeyEvent(event.nativeEvent)
349
350
  ) {
350
351
  event.preventDefault();
351
352
  submit();
@@ -77,6 +77,12 @@ function stringField(record: UnknownRecord, ...keys: string[]): string {
77
77
  return "";
78
78
  }
79
79
 
80
+ function stringArray(value: unknown): string[] {
81
+ return Array.isArray(value)
82
+ ? value.filter((item): item is string => typeof item === "string")
83
+ : [];
84
+ }
85
+
80
86
  function numberField(record: UnknownRecord, ...keys: string[]): number | undefined {
81
87
  for (const key of keys) {
82
88
  const value = record[key];
@@ -342,17 +348,7 @@ function fileSummary(
342
348
  }
343
349
 
344
350
  function questionsOf(input: UnknownRecord): UnknownRecord[] {
345
- if (Array.isArray(input.questions)) return input.questions.map(recordOf);
346
- if (typeof input.question === "string") {
347
- return [
348
- {
349
- question: input.question,
350
- header: input.header,
351
- options: input.options,
352
- },
353
- ];
354
- }
355
- return [];
351
+ return Array.isArray(input.questions) ? input.questions.map(recordOf) : [];
356
352
  }
357
353
 
358
354
  function summaryFor(
@@ -1113,7 +1109,10 @@ function SkillDetails({
1113
1109
 
1114
1110
  function AskUserDetails({ presentation }: CamelToolDetailsProps) {
1115
1111
  const questions = questionsOf(presentation.input);
1116
- const answer = presentation.outputText;
1112
+ const output = recordOf(presentation.output);
1113
+ const answers = Array.isArray(output.answers)
1114
+ ? output.answers.map(recordOf)
1115
+ : [];
1117
1116
  return (
1118
1117
  <div className="space-y-2">
1119
1118
  {questions.length > 0 ? (
@@ -1123,7 +1122,12 @@ function AskUserDetails({ presentation }: CamelToolDetailsProps) {
1123
1122
  {stringField(question, "question", "prompt") || "Question"}
1124
1123
  </p>
1125
1124
  <p className="text-muted-foreground/60">
1126
- {answer || "—"}
1125
+ {[
1126
+ ...stringArray(answers[index]?.selections),
1127
+ ...(typeof answers[index]?.text === "string" && answers[index].text
1128
+ ? [answers[index].text]
1129
+ : []),
1130
+ ].join(" / ") || "—"}
1127
1131
  </p>
1128
1132
  </div>
1129
1133
  ))
@@ -41,7 +41,6 @@ export interface CamelAssistantTurn {
41
41
  };
42
42
  stepCount: number;
43
43
  copyText: string;
44
- replyText?: string;
45
44
  durationMs?: number;
46
45
  completedAt?: number;
47
46
  }
@@ -115,10 +114,6 @@ function standaloneKind(message: UIMessage): CamelStandaloneKind | null {
115
114
  return message.role === "user" ? "user" : null;
116
115
  }
117
116
 
118
- function isDirectUserMessage(message: UIMessage): boolean {
119
- return message.role === "user" && standaloneKind(message) === "user";
120
- }
121
-
122
117
  export function camelMessageText(message: UIMessage): string {
123
118
  return message.parts
124
119
  .filter(isVisibleText)
@@ -228,7 +223,6 @@ function turnDuration(message: UIMessage): number | undefined {
228
223
  export function projectCamelAssistantTurn(
229
224
  message: UIMessage,
230
225
  finalToolNames: readonly string[] = [],
231
- replyText?: string,
232
226
  ): CamelAssistantTurn {
233
227
  const parts = message.parts.map((part, partIndex) => ({
234
228
  messageId: message.id,
@@ -294,7 +288,6 @@ export function projectCamelAssistantTurn(
294
288
  parts.filter(({ part }) => part.type === "step-start").length,
295
289
  ),
296
290
  copyText: camelMessageText(message),
297
- replyText,
298
291
  durationMs: turnDuration(message),
299
292
  completedAt: camelCompletedAt(message),
300
293
  };
@@ -309,16 +302,7 @@ export function projectCamelMessages(
309
302
  if (message.role === "system") return;
310
303
  const displayKind = standaloneKind(message);
311
304
  if (message.role === "assistant" && displayKind === null) {
312
- const reply = messages
313
- .slice(index + 1)
314
- .find(isDirectUserMessage);
315
- items.push(
316
- projectCamelAssistantTurn(
317
- message,
318
- finalToolNames,
319
- reply ? camelMessageText(reply).trim() || undefined : undefined,
320
- ),
321
- );
305
+ items.push(projectCamelAssistantTurn(message, finalToolNames));
322
306
  return;
323
307
  }
324
308
  if (displayKind !== null) {
@@ -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-full overflow-hidden rounded-2xl border border-border bg-popover shadow-card",
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.isComposing) ||
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.isComposing
251
+ !isComposingKeyEvent(event.nativeEvent)
251
252
  ) {
252
253
  event.preventDefault();
253
254
  if (canSend) void submit();
@@ -3,4 +3,5 @@ export * from "./composer-attachments";
3
3
  export * from "./composer-trigger-popover";
4
4
  export * from "./chat-composer";
5
5
  export * from "./drop-controller";
6
+ export * from "./key-rules";
6
7
  export * from "./types";
@@ -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/contracts.ts CHANGED
@@ -47,8 +47,6 @@ export interface PartRendererContext {
47
47
  activityId: string;
48
48
  isActive: boolean;
49
49
  isLatestOfKind: boolean;
50
- /** First user message after the assistant message that owns this part. */
51
- replyText?: string;
52
50
  approval?: ToolApprovalView;
53
51
  status: MessagePanelStatus;
54
52
  actions: MessagePanelActions;
@@ -93,7 +93,6 @@ interface MessageRowProps<T> {
93
93
  slots: MessagePanelSlots;
94
94
  status: MessagePanelProps<T>["status"];
95
95
  latestActivityByKind: ReadonlyMap<string, string>;
96
- replyText?: string;
97
96
  }
98
97
 
99
98
  function MessageRow<T>({
@@ -107,7 +106,6 @@ function MessageRow<T>({
107
106
  slots,
108
107
  status,
109
108
  latestActivityByKind,
110
- replyText,
111
109
  }: MessageRowProps<T>) {
112
110
  const role = roleOf(adapter, message);
113
111
  const parts = adapter.getParts(message);
@@ -186,7 +184,6 @@ function MessageRow<T>({
186
184
  isLatestOfKind:
187
185
  key === undefined ||
188
186
  latestActivityByKind.get(key) === activityId,
189
- replyText,
190
187
  approval: adapter.getToolApproval?.(part),
191
188
  status,
192
189
  actions,
@@ -255,25 +252,6 @@ function MessagePanelInner<T>(
255
252
  });
256
253
  return latest;
257
254
  }, [adapter, ids, messages]);
258
- const replyTextByMessageId = useMemo(() => {
259
- const replies = new Map<string, string>();
260
- messages.forEach((message, messageIndex) => {
261
- if (roleOf(adapter, message) !== "assistant") return;
262
- for (
263
- let replyIndex = messageIndex + 1;
264
- replyIndex < messages.length;
265
- replyIndex += 1
266
- ) {
267
- const reply = messages[replyIndex];
268
- if (roleOf(adapter, reply) !== "user") continue;
269
- const text = messageText(adapter, reply).trim();
270
- if (text) replies.set(ids[messageIndex], text);
271
- break;
272
- }
273
- });
274
- return replies;
275
- }, [adapter, ids, messages]);
276
-
277
255
  return (
278
256
  <div
279
257
  ref={ref}
@@ -332,7 +310,6 @@ function MessagePanelInner<T>(
332
310
  slots={slots}
333
311
  status={status}
334
312
  latestActivityByKind={latestActivityByKind}
335
- replyText={replyTextByMessageId.get(ids[index])}
336
313
  />
337
314
  ))}
338
315
  {showPending && (