@springbrand/message-panel 0.1.3-alpha.3 → 0.1.3-alpha.30

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 (39) 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/assets/thinking-spark.svg +9 -0
  8. package/cloud-os/capability-chip.tsx +35 -0
  9. package/cloud-os/chat/activity-indicator.tsx +2 -2
  10. package/cloud-os/chat/cloud-os-chat-messages.tsx +374 -63
  11. package/cloud-os/chat/image-generation.tsx +76 -0
  12. package/cloud-os/chat/loading-state.tsx +25 -0
  13. package/cloud-os/chat/markdown-message.tsx +144 -41
  14. package/cloud-os/chat/rich-blocks.tsx +548 -246
  15. package/cloud-os/chat/tool-presentation.ts +35 -16
  16. package/cloud-os/chat/tool-rows.tsx +330 -85
  17. package/cloud-os/chat/transcript-model.ts +428 -56
  18. package/cloud-os/composer/cloud-os-chat-input.tsx +228 -142
  19. package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
  20. package/cloud-os/file-view.tsx +441 -0
  21. package/cloud-os/index.ts +35 -3
  22. package/cloud-os/layout/cloud-os-workspace-split.tsx +58 -15
  23. package/cloud-os/primitives/workshop-controls.tsx +2 -2
  24. package/cloud-os/styles/cloud-os.css +100 -20
  25. package/demo/camel-chat-showcase.tsx +21 -13
  26. package/demo/chat-scenarios.ts +223 -43
  27. package/demo/cloud-os-chat-showcase.tsx +47 -14
  28. package/demo/fixtures.ts +4 -5
  29. package/demo/message-panel-gallery.tsx +16 -2
  30. package/package.json +3 -4
  31. package/src/camel/camel-chat-messages.tsx +78 -78
  32. package/src/camel/camel-tool-presentation.tsx +19 -15
  33. package/src/camel/camel-turn.ts +1 -17
  34. package/src/composer/composer-attachments.tsx +1 -1
  35. package/src/composer/composer-trigger-popover.tsx +1 -1
  36. package/src/composer/composer.tsx +2 -2
  37. package/src/contracts.ts +0 -2
  38. package/src/message-panel.tsx +1 -24
  39. package/src/parts/index.tsx +103 -64
@@ -13,6 +13,7 @@ import {
13
13
  CloudOsRoot,
14
14
  CloudOsWorkspacePanel,
15
15
  CloudOsWorkspaceSplit,
16
+ ScheduleConfirmation,
16
17
  WorkshopIconButton,
17
18
  buildCloudOsEntries,
18
19
  type CloudOsAttachmentView,
@@ -20,17 +21,19 @@ import {
20
21
  } from "../cloud-os";
21
22
  import {
22
23
  defaultPrompt,
24
+ demoAskUserResponse,
23
25
  demoModels,
24
26
  flowSteps,
25
27
  messagesForScenario,
26
28
  scenarios,
27
29
  type DemoScenario,
30
+ type DemoAskUserResponse,
28
31
  type DemoScenarioMeta,
29
32
  } from "./chat-scenarios";
30
33
 
31
34
  /**
32
35
  * cloud-os 版快速预览。场景表和 camel-chat-showcase 共用 `chat-scenarios.ts`,
33
- * 所以两边的状态逐条对齐(同样 22 个场景、同样的 7 步流程条、同样的初始 URL 参数)。
36
+ * 所以两边的状态逐条对齐(同样 23 个场景、同样的 7 步流程条、同样的初始 URL 参数)。
34
37
  * 差别只在外壳:这里是 cloudflare-os 的左聊天 / 右工作区双栏。
35
38
  */
36
39
 
@@ -122,7 +125,7 @@ export function CloudOsChatShowcase() {
122
125
  const [mode, setMode] = useState<CloudOsMode>("light");
123
126
  const [scenario, setScenario] = useState<DemoScenario>(initialScenario);
124
127
  const [prompt, setPrompt] = useState(defaultPrompt);
125
- const [humanAnswer, setHumanAnswer] = useState("管理层");
128
+ const [humanAnswer, setHumanAnswer] = useState(demoAskUserResponse);
126
129
  const [model, setModel] = useState<string | null>(cloudOsModels[0].id);
127
130
  const [value, setValue] = useState("");
128
131
  const [attachments, setAttachments] = useState<CloudOsAttachmentView[]>([]);
@@ -153,7 +156,8 @@ export function CloudOsChatShowcase() {
153
156
  ? (part as { type?: unknown; state?: unknown })
154
157
  : {};
155
158
  return (record.type === "tool-write_file" ||
156
- record.type === "tool-cancel_schedule") &&
159
+ record.type === "tool-cancel_schedule" ||
160
+ record.type === "tool-schedule") &&
157
161
  record.state === "approval-requested"
158
162
  ? "cloud-os-demo-approval"
159
163
  : undefined;
@@ -283,7 +287,7 @@ export function CloudOsChatShowcase() {
283
287
 
284
288
  <CloudOsWorkspaceSplit
285
289
  workspaceOpen={workspaceOpen}
286
- isAgentActive={status === "submitted" || status === "streaming"}
290
+ isLoading={scenario === "hydrating"}
287
291
  chat={
288
292
  <div className="flex h-full min-h-0 flex-col bg-kumo-base">
289
293
  <aside
@@ -348,11 +352,40 @@ export function CloudOsChatShowcase() {
348
352
  <CloudOsChatMessages
349
353
  messages={messages}
350
354
  status={status}
351
- isHydrating={scenario === "hydrating"}
355
+ turnActive={
356
+ status === "submitted" ||
357
+ status === "streaming" ||
358
+ scenario === "ask" ||
359
+ scenario === "schedule_create" ||
360
+ ((scenario === "approval" || scenario === "schedule_cancel") &&
361
+ approval === undefined)
362
+ }
352
363
  isRecovering={scenario === "recovering"}
353
- recoveryStatusLabel="模型响应暂时中断,正在重试…"
364
+ recoveryStatusLabel="Model response interrupted. Retrying…"
365
+ awaitingApproval={
366
+ (scenario === "approval" ||
367
+ scenario === "schedule_cancel" ||
368
+ scenario === "schedule_create") &&
369
+ approval === undefined
370
+ }
354
371
  showThinkingTraces={showThinkingTraces}
355
372
  approvalIdOf={approvalIdOf}
373
+ renderApproval={({ toolName, input }) =>
374
+ toolName === "schedule"
375
+ ? (
376
+ <ScheduleConfirmation
377
+ input={input}
378
+ onCancel={() => {
379
+ selectScenario("submitted");
380
+ setLastEvent("Schedule creation cancelled");
381
+ }}
382
+ onConfirm={() => {
383
+ selectScenario("schedule");
384
+ setLastEvent("Scheduled task created");
385
+ }}
386
+ />
387
+ )
388
+ : undefined}
356
389
  error={statusError}
357
390
  constrainWidth={!workspaceOpen}
358
391
  onRetry={() => {
@@ -379,17 +412,17 @@ export function CloudOsChatShowcase() {
379
412
  onRespond={async (_toolCallId, response) => {
380
413
  const data =
381
414
  response && typeof response === "object"
382
- ? (response as { selections?: string[]; text?: string })
383
- : {};
384
- const answer = [
385
- ...(data.selections ?? []),
386
- data.text?.trim() ?? "",
387
- ]
415
+ ? (response as DemoAskUserResponse)
416
+ : { answers: [] };
417
+ const answer = data.answers.flatMap((item) => [
418
+ ...item.selections,
419
+ item.text?.trim() ?? "",
420
+ ])
388
421
  .filter(Boolean)
389
422
  .join(" / ");
390
- if (answer) setHumanAnswer(answer);
423
+ if (data.answers.length > 0) setHumanAnswer(data);
391
424
  selectScenario("planning");
392
- setLastEvent(`已回答并继续:${answer || humanAnswer}`);
425
+ setLastEvent(`已回答并继续:${answer}`);
393
426
  return true;
394
427
  }}
395
428
  />
package/demo/fixtures.ts CHANGED
@@ -166,17 +166,16 @@ export const allPartsMessage: DemoMessage = {
166
166
  type: "ask_user",
167
167
  questions: [
168
168
  {
169
- prompt: "报告通过什么渠道发送?",
170
- kind: "single",
169
+ question: "报告通过什么渠道发送?",
171
170
  options: ["Email", "Slack", "Both"],
172
171
  },
173
172
  {
174
- prompt: "需要包含哪些内容?",
175
- kind: "multi",
173
+ question: "需要包含哪些内容?",
176
174
  options: ["定价", "功能", "动态"],
175
+ multiSelect: true,
176
+ allowCustom: true,
177
177
  },
178
178
  ],
179
- freeTextPlaceholder: "补充竞争对手名称…",
180
179
  },
181
180
  {
182
181
  type: "suggestions",
@@ -220,6 +220,7 @@ function ComposerInteractionHarness() {
220
220
 
221
221
  export function MessagePanelGallery() {
222
222
  const [theme, setTheme] = useState<"light" | "dark">("dark");
223
+ const [askUserOutput, setAskUserOutput] = useState<unknown>();
223
224
  const rootClass = theme === "dark" ? "dark" : "";
224
225
  useEffect(() => {
225
226
  document.documentElement.classList.toggle("dark", theme === "dark");
@@ -309,12 +310,25 @@ export function MessagePanelGallery() {
309
310
  />
310
311
  <div style={{ height: 1100, overflow: "hidden", border: "1px solid var(--border)", borderRadius: 18 }}>
311
312
  <MessagePanel
312
- messages={[basicMessages[0], allPartsMessage]}
313
+ messages={[
314
+ basicMessages[0],
315
+ {
316
+ ...allPartsMessage,
317
+ parts: allPartsMessage.parts.map((part) =>
318
+ part.type === "ask_user" && askUserOutput
319
+ ? { ...part, output: askUserOutput }
320
+ : part
321
+ ),
322
+ },
323
+ ]}
313
324
  status="ready"
314
325
  adapter={demoMessageAdapter}
315
326
  actions={{
316
327
  onSuggestion: noOp,
317
- onAnswer: noOp,
328
+ onAnswer: async (output) => {
329
+ await new Promise((resolve) => setTimeout(resolve, 250));
330
+ setAskUserOutput(output);
331
+ },
318
332
  onApprove: noOp,
319
333
  onPreview: noOp,
320
334
  onDownload: noOp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/message-panel",
3
- "version": "0.1.3-alpha.3",
3
+ "version": "0.1.3-alpha.30",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -31,11 +31,10 @@
31
31
  "@streamdown/math": "^1.0.2",
32
32
  "@streamdown/mermaid": "^1.0.2",
33
33
  "lucide-react": "^1.24.0",
34
+ "motion": "^13.1.0",
34
35
  "radix-ui": "^1.6.2",
35
- "react-markdown": "^10.1.0",
36
- "remark-gfm": "^4.0.1",
37
36
  "streamdown": "^2.5.0",
38
- "thinking-orbs": "0.1.1",
37
+ "thinking-orbs": "0.2.0",
39
38
  "use-stick-to-bottom": "^1.1.6"
40
39
  },
41
40
  "devDependencies": {
@@ -167,7 +167,7 @@ export function CamelChatError({
167
167
  }
168
168
 
169
169
  export function CamelChatRecovery({
170
- label = "模型响应暂时中断,正在重试…",
170
+ label = "The model response was interrupted. Retrying…",
171
171
  }: {
172
172
  label?: string;
173
173
  }) {
@@ -668,53 +668,30 @@ function CamelSchedule({
668
668
 
669
669
  function CamelAskUser({
670
670
  part,
671
- submittedAnswer,
672
671
  onAnswer,
673
672
  }: {
674
673
  part: unknown;
675
- submittedAnswer?: string;
676
674
  onAnswer: (payload: unknown) => void;
677
675
  }) {
678
676
  const record = recordOf(part);
679
677
  const data = dataOf(part);
680
- const output = record.output;
681
678
  const questions = Array.isArray(data.questions)
682
679
  ? data.questions.map(recordOf)
683
- : typeof data.question === "string"
684
- ? [{
685
- prompt: data.question,
686
- kind: data.multiSelect === true ? "multi" : "single",
687
- options: Array.isArray(data.options)
688
- ? data.options.filter(
689
- (option): option is string => typeof option === "string",
690
- )
691
- : [],
692
- }]
693
- : [];
694
- const [selections, setSelections] = useState<string[][]>(() =>
695
- questions.map(() => []),
696
- );
697
- const [text, setText] = useState("");
698
- const [submitted, setSubmitted] = useState(false);
699
- const outputRecord = recordOf(output);
700
- const externalAnswer =
701
- submittedAnswer?.trim() ||
702
- (typeof output === "string"
703
- ? output.trim()
704
- : typeof outputRecord.text === "string"
705
- ? outputRecord.text.trim()
706
- : "");
707
- const externalSelections = new Set(
708
- externalAnswer
709
- .split("、")
710
- .map((value) => value.trim())
711
- .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()
712
694
  );
713
- const resolvedSubmitted = submitted || Boolean(externalAnswer);
714
- const valid = questions.every((question, index) => {
715
- const options = Array.isArray(question.options) ? question.options : [];
716
- return options.length === 0 || (selections[index]?.length ?? 0) > 0;
717
- });
718
695
 
719
696
  return (
720
697
  <section
@@ -723,21 +700,28 @@ function CamelAskUser({
723
700
  data-state={resolvedSubmitted ? "submitted" : "idle"}
724
701
  >
725
702
  {questions.map((question, questionIndex) => {
726
- const prompt = String(question.prompt ?? "Choose an option");
703
+ const prompt = String(question.question ?? "Choose an option");
727
704
  const options = Array.isArray(question.options)
728
705
  ? question.options.filter(
729
706
  (option): option is string => typeof option === "string",
730
707
  )
731
708
  : [];
732
- 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
+ : [];
733
717
  return (
734
718
  <div className="space-y-2" key={`${prompt}:${questionIndex}`}>
735
719
  <p className="text-sm text-foreground">{prompt}</p>
736
720
  <div className="flex flex-wrap gap-2">
737
721
  {options.map((option) => {
738
- const selected = externalAnswer
739
- ? externalSelections.has(option)
740
- : selections[questionIndex]?.includes(option);
722
+ const selected = settledAnswer
723
+ ? settledSelections.includes(option)
724
+ : answerAt(questionIndex).selections.includes(option);
741
725
  return (
742
726
  <Button
743
727
  key={option}
@@ -746,15 +730,21 @@ function CamelAskUser({
746
730
  variant={selected ? "default" : "outline"}
747
731
  disabled={resolvedSubmitted}
748
732
  onClick={() =>
749
- setSelections((current) =>
750
- current.map((values, index) => {
751
- if (index !== questionIndex) return values;
752
- if (!multi) return [option];
753
- return values.includes(option)
754
- ? values.filter((value) => value !== option)
755
- : [...values, option];
756
- }),
757
- )
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
+ })
758
748
  }
759
749
  >
760
750
  {option}
@@ -762,34 +752,54 @@ function CamelAskUser({
762
752
  );
763
753
  })}
764
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
+ )}
765
785
  </div>
766
786
  );
767
787
  })}
768
- {typeof data.freeTextPlaceholder === "string" && (
769
- <input
770
- value={text}
771
- disabled={resolvedSubmitted}
772
- onChange={(event) => setText(event.currentTarget.value)}
773
- placeholder={data.freeTextPlaceholder}
774
- 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"
775
- />
776
- )}
777
788
  <Button
778
789
  type="button"
779
790
  size="sm"
780
791
  disabled={resolvedSubmitted || !valid}
781
792
  onClick={() => {
782
- onAnswer({ selections, text });
783
- setSubmitted(true);
793
+ onAnswer({
794
+ answers: questions.map((_, index) => ({
795
+ selections: answerAt(index).selections,
796
+ text: answerAt(index).text.trim(),
797
+ })),
798
+ });
784
799
  }}
785
800
  >
786
801
  {resolvedSubmitted ? "Submitted" : "Submit"}
787
802
  </Button>
788
- {externalAnswer && (
789
- <p className="text-xs text-muted-foreground">
790
- 已回答:{externalAnswer}
791
- </p>
792
- )}
793
803
  </section>
794
804
  );
795
805
  }
@@ -800,7 +810,6 @@ function CamelRichPart({
800
810
  onApprove,
801
811
  onSuggestion,
802
812
  onAnswer,
803
- replyText,
804
813
  getToolApproval,
805
814
  }: {
806
815
  part: unknown;
@@ -808,7 +817,6 @@ function CamelRichPart({
808
817
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
809
818
  onSuggestion: (text: string) => void;
810
819
  onAnswer: (payload: unknown) => void;
811
- replyText?: string;
812
820
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
813
821
  }) {
814
822
  const kind = normalizedPartType(part);
@@ -824,7 +832,6 @@ function CamelRichPart({
824
832
  return (
825
833
  <CamelAskUser
826
834
  part={part}
827
- submittedAnswer={replyText}
828
835
  onAnswer={onAnswer}
829
836
  />
830
837
  );
@@ -924,7 +931,6 @@ function AssistantPart({
924
931
  onApprove,
925
932
  onSuggestion,
926
933
  onAnswer,
927
- replyText,
928
934
  getToolApproval,
929
935
  resolveUrl,
930
936
  }: {
@@ -933,7 +939,6 @@ function AssistantPart({
933
939
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
934
940
  onSuggestion: (text: string) => void;
935
941
  onAnswer: (payload: unknown) => void;
936
- replyText?: string;
937
942
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
938
943
  resolveUrl?: MessageUrlResolver;
939
944
  }) {
@@ -960,7 +965,6 @@ function AssistantPart({
960
965
  onApprove={onApprove}
961
966
  onSuggestion={onSuggestion}
962
967
  onAnswer={onAnswer}
963
- replyText={replyText}
964
968
  getToolApproval={getToolApproval}
965
969
  />
966
970
  );
@@ -1137,7 +1141,6 @@ function RenderAssistantPart({
1137
1141
  onApprove,
1138
1142
  onSuggestion,
1139
1143
  onAnswer,
1140
- replyText,
1141
1144
  getToolApproval,
1142
1145
  resolveUrl,
1143
1146
  }: {
@@ -1146,7 +1149,6 @@ function RenderAssistantPart({
1146
1149
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
1147
1150
  onSuggestion: (text: string) => void;
1148
1151
  onAnswer: (payload: unknown) => void;
1149
- replyText?: string;
1150
1152
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
1151
1153
  resolveUrl?: MessageUrlResolver;
1152
1154
  }) {
@@ -1160,7 +1162,6 @@ function RenderAssistantPart({
1160
1162
  onApprove={onApprove}
1161
1163
  onSuggestion={onSuggestion}
1162
1164
  onAnswer={onAnswer}
1163
- replyText={replyText}
1164
1165
  getToolApproval={getToolApproval}
1165
1166
  resolveUrl={resolveUrl}
1166
1167
  />
@@ -1200,7 +1201,6 @@ function AssistantTurn({
1200
1201
  onApprove={onApprove}
1201
1202
  onSuggestion={onSuggestion}
1202
1203
  onAnswer={onAnswer}
1203
- replyText={turn.replyText}
1204
1204
  getToolApproval={getToolApproval}
1205
1205
  resolveUrl={resolveUrl}
1206
1206
  />
@@ -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) {
@@ -59,7 +59,7 @@ export const AttachmentPreview = forwardRef<
59
59
  attachment.status === "uploading"
60
60
  ? "上传中…"
61
61
  : attachment.status === "error"
62
- ? "上传失败"
62
+ ? "Upload failed"
63
63
  : [attachment.note, formatSize(attachment.size)].filter(Boolean).join(" · ");
64
64
  return (
65
65
  <div
@@ -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
  )}
@@ -211,10 +211,10 @@ function ComposerInner<TAttachment>(
211
211
  );
212
212
  const invalid = selected.filter((file) => !valid.includes(file));
213
213
  if (all.length > selected.length) {
214
- onValidationError?.(`最多只能添加 ${maxFiles ?? 1} 个附件`, all);
214
+ onValidationError?.(`You can add up to ${maxFiles ?? 1} attachments`, all);
215
215
  }
216
216
  if (invalid.length) {
217
- onValidationError?.("存在不支持或超过大小限制的文件", invalid);
217
+ onValidationError?.("Some files are unsupported or exceed the size limit", invalid);
218
218
  }
219
219
  if (valid.length) void onFilesAdded?.(valid);
220
220
  },
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;