@springbrand/message-panel 0.1.3-alpha.2 → 0.1.3-alpha.21

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 (41) hide show
  1. package/cloud-os/README.md +6 -6
  2. package/cloud-os/assets/followup-arrow.svg +3 -0
  3. package/cloud-os/assets/loading-corner.svg +3 -0
  4. package/cloud-os/assets/loading-mark.svg +16 -0
  5. package/cloud-os/assets/loading-spark.svg +3 -0
  6. package/cloud-os/assets/model-selected.svg +5 -0
  7. package/cloud-os/capability-chip.tsx +35 -0
  8. package/cloud-os/chat/activity-indicator.tsx +29 -0
  9. package/cloud-os/chat/cloud-os-chat-messages.tsx +123 -69
  10. package/cloud-os/chat/markdown-message.tsx +82 -40
  11. package/cloud-os/chat/rich-blocks.tsx +494 -190
  12. package/cloud-os/chat/tool-presentation.ts +67 -6
  13. package/cloud-os/chat/tool-rows.tsx +87 -43
  14. package/cloud-os/chat/transcript-model.ts +215 -23
  15. package/cloud-os/composer/cloud-os-chat-input.tsx +242 -127
  16. package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
  17. package/cloud-os/file-view.tsx +266 -0
  18. package/cloud-os/index.ts +29 -2
  19. package/cloud-os/layout/cloud-os-workspace-split.tsx +107 -24
  20. package/cloud-os/primitives/workshop-controls.tsx +2 -2
  21. package/cloud-os/styles/cloud-os.css +93 -19
  22. package/demo/camel-chat-showcase.tsx +21 -13
  23. package/demo/chat-scenarios.ts +100 -40
  24. package/demo/cloud-os-chat-showcase.tsx +51 -17
  25. package/demo/fixtures.ts +4 -5
  26. package/demo/message-panel-gallery.tsx +16 -2
  27. package/package.json +3 -4
  28. package/src/camel/camel-chat-messages.tsx +82 -77
  29. package/src/camel/camel-prompt-input.tsx +2 -1
  30. package/src/camel/camel-tool-presentation.tsx +19 -15
  31. package/src/camel/camel-turn.ts +1 -17
  32. package/src/chat-summary-panel.tsx +1 -1
  33. package/src/composer/chat-composer.tsx +2 -1
  34. package/src/composer/composer-trigger-popover.tsx +1 -1
  35. package/src/composer/composer.tsx +2 -1
  36. package/src/composer/index.ts +1 -0
  37. package/src/composer/key-rules.ts +12 -0
  38. package/src/contracts.ts +0 -2
  39. package/src/message-panel.tsx +0 -23
  40. package/src/parts/index.tsx +102 -63
  41. 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,
@@ -663,53 +668,30 @@ function CamelSchedule({
663
668
 
664
669
  function CamelAskUser({
665
670
  part,
666
- submittedAnswer,
667
671
  onAnswer,
668
672
  }: {
669
673
  part: unknown;
670
- submittedAnswer?: string;
671
674
  onAnswer: (payload: unknown) => void;
672
675
  }) {
673
676
  const record = recordOf(part);
674
677
  const data = dataOf(part);
675
- const output = record.output;
676
678
  const questions = Array.isArray(data.questions)
677
679
  ? data.questions.map(recordOf)
678
- : typeof data.question === "string"
679
- ? [{
680
- prompt: data.question,
681
- kind: data.multiSelect === true ? "multi" : "single",
682
- options: Array.isArray(data.options)
683
- ? data.options.filter(
684
- (option): option is string => typeof option === "string",
685
- )
686
- : [],
687
- }]
688
- : [];
689
- const [selections, setSelections] = useState<string[][]>(() =>
690
- questions.map(() => []),
691
- );
692
- const [text, setText] = useState("");
693
- const [submitted, setSubmitted] = useState(false);
694
- const outputRecord = recordOf(output);
695
- const externalAnswer =
696
- submittedAnswer?.trim() ||
697
- (typeof output === "string"
698
- ? output.trim()
699
- : typeof outputRecord.text === "string"
700
- ? outputRecord.text.trim()
701
- : "");
702
- const externalSelections = new Set(
703
- externalAnswer
704
- .split("、")
705
- .map((value) => value.trim())
706
- .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()
707
694
  );
708
- const resolvedSubmitted = submitted || Boolean(externalAnswer);
709
- const valid = questions.every((question, index) => {
710
- const options = Array.isArray(question.options) ? question.options : [];
711
- return options.length === 0 || (selections[index]?.length ?? 0) > 0;
712
- });
713
695
 
714
696
  return (
715
697
  <section
@@ -718,21 +700,28 @@ function CamelAskUser({
718
700
  data-state={resolvedSubmitted ? "submitted" : "idle"}
719
701
  >
720
702
  {questions.map((question, questionIndex) => {
721
- const prompt = String(question.prompt ?? "Choose an option");
703
+ const prompt = String(question.question ?? "Choose an option");
722
704
  const options = Array.isArray(question.options)
723
705
  ? question.options.filter(
724
706
  (option): option is string => typeof option === "string",
725
707
  )
726
708
  : [];
727
- 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
+ : [];
728
717
  return (
729
718
  <div className="space-y-2" key={`${prompt}:${questionIndex}`}>
730
719
  <p className="text-sm text-foreground">{prompt}</p>
731
720
  <div className="flex flex-wrap gap-2">
732
721
  {options.map((option) => {
733
- const selected = externalAnswer
734
- ? externalSelections.has(option)
735
- : selections[questionIndex]?.includes(option);
722
+ const selected = settledAnswer
723
+ ? settledSelections.includes(option)
724
+ : answerAt(questionIndex).selections.includes(option);
736
725
  return (
737
726
  <Button
738
727
  key={option}
@@ -741,15 +730,21 @@ function CamelAskUser({
741
730
  variant={selected ? "default" : "outline"}
742
731
  disabled={resolvedSubmitted}
743
732
  onClick={() =>
744
- setSelections((current) =>
745
- current.map((values, index) => {
746
- if (index !== questionIndex) return values;
747
- if (!multi) return [option];
748
- return values.includes(option)
749
- ? values.filter((value) => value !== option)
750
- : [...values, option];
751
- }),
752
- )
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
+ })
753
748
  }
754
749
  >
755
750
  {option}
@@ -757,34 +752,54 @@ function CamelAskUser({
757
752
  );
758
753
  })}
759
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
+ )}
760
785
  </div>
761
786
  );
762
787
  })}
763
- {typeof data.freeTextPlaceholder === "string" && (
764
- <input
765
- value={text}
766
- disabled={resolvedSubmitted}
767
- onChange={(event) => setText(event.currentTarget.value)}
768
- placeholder={data.freeTextPlaceholder}
769
- 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"
770
- />
771
- )}
772
788
  <Button
773
789
  type="button"
774
790
  size="sm"
775
791
  disabled={resolvedSubmitted || !valid}
776
792
  onClick={() => {
777
- onAnswer({ selections, text });
778
- setSubmitted(true);
793
+ onAnswer({
794
+ answers: questions.map((_, index) => ({
795
+ selections: answerAt(index).selections,
796
+ text: answerAt(index).text.trim(),
797
+ })),
798
+ });
779
799
  }}
780
800
  >
781
801
  {resolvedSubmitted ? "Submitted" : "Submit"}
782
802
  </Button>
783
- {externalAnswer && (
784
- <p className="text-xs text-muted-foreground">
785
- 已回答:{externalAnswer}
786
- </p>
787
- )}
788
803
  </section>
789
804
  );
790
805
  }
@@ -795,7 +810,6 @@ function CamelRichPart({
795
810
  onApprove,
796
811
  onSuggestion,
797
812
  onAnswer,
798
- replyText,
799
813
  getToolApproval,
800
814
  }: {
801
815
  part: unknown;
@@ -803,7 +817,6 @@ function CamelRichPart({
803
817
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
804
818
  onSuggestion: (text: string) => void;
805
819
  onAnswer: (payload: unknown) => void;
806
- replyText?: string;
807
820
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
808
821
  }) {
809
822
  const kind = normalizedPartType(part);
@@ -819,7 +832,6 @@ function CamelRichPart({
819
832
  return (
820
833
  <CamelAskUser
821
834
  part={part}
822
- submittedAnswer={replyText}
823
835
  onAnswer={onAnswer}
824
836
  />
825
837
  );
@@ -919,7 +931,6 @@ function AssistantPart({
919
931
  onApprove,
920
932
  onSuggestion,
921
933
  onAnswer,
922
- replyText,
923
934
  getToolApproval,
924
935
  resolveUrl,
925
936
  }: {
@@ -928,7 +939,6 @@ function AssistantPart({
928
939
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
929
940
  onSuggestion: (text: string) => void;
930
941
  onAnswer: (payload: unknown) => void;
931
- replyText?: string;
932
942
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
933
943
  resolveUrl?: MessageUrlResolver;
934
944
  }) {
@@ -955,7 +965,6 @@ function AssistantPart({
955
965
  onApprove={onApprove}
956
966
  onSuggestion={onSuggestion}
957
967
  onAnswer={onAnswer}
958
- replyText={replyText}
959
968
  getToolApproval={getToolApproval}
960
969
  />
961
970
  );
@@ -1132,7 +1141,6 @@ function RenderAssistantPart({
1132
1141
  onApprove,
1133
1142
  onSuggestion,
1134
1143
  onAnswer,
1135
- replyText,
1136
1144
  getToolApproval,
1137
1145
  resolveUrl,
1138
1146
  }: {
@@ -1141,7 +1149,6 @@ function RenderAssistantPart({
1141
1149
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
1142
1150
  onSuggestion: (text: string) => void;
1143
1151
  onAnswer: (payload: unknown) => void;
1144
- replyText?: string;
1145
1152
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
1146
1153
  resolveUrl?: MessageUrlResolver;
1147
1154
  }) {
@@ -1155,7 +1162,6 @@ function RenderAssistantPart({
1155
1162
  onApprove={onApprove}
1156
1163
  onSuggestion={onSuggestion}
1157
1164
  onAnswer={onAnswer}
1158
- replyText={replyText}
1159
1165
  getToolApproval={getToolApproval}
1160
1166
  resolveUrl={resolveUrl}
1161
1167
  />
@@ -1195,7 +1201,6 @@ function AssistantTurn({
1195
1201
  onApprove={onApprove}
1196
1202
  onSuggestion={onSuggestion}
1197
1203
  onAnswer={onAnswer}
1198
- replyText={turn.replyText}
1199
1204
  getToolApproval={getToolApproval}
1200
1205
  resolveUrl={resolveUrl}
1201
1206
  />
@@ -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(
@@ -387,7 +383,7 @@ function summaryFor(
387
383
  complete: "Edited",
388
384
  });
389
385
  case "bash": {
390
- const command = stringField(input, "command", "cmd");
386
+ const command = stringField(input, "script", "command", "cmd");
391
387
  const description = stringField(input, "description");
392
388
  const label = description || truncate(command || "command", 30);
393
389
  return running
@@ -822,7 +818,7 @@ function EditDetails({ presentation }: CamelToolDetailsProps) {
822
818
 
823
819
  function BashDetails({ presentation }: CamelToolDetailsProps) {
824
820
  const { input, output, outputText } = presentation;
825
- const command = stringField(input, "command", "cmd");
821
+ const command = stringField(input, "script", "command", "cmd");
826
822
  const outputRecord = recordOf(output);
827
823
  const parsedFromText = (() => {
828
824
  if (!outputText.trim().startsWith("{")) return {};
@@ -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 && (