@springbrand/message-panel 0.1.3-alpha.9 → 0.2.0-alpha.38

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 (67) hide show
  1. package/cloud-os/README.md +4 -3
  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-star.svg +9 -0
  8. package/cloud-os/capability-chip.tsx +1 -1
  9. package/cloud-os/chat/cloud-os-chat-messages.tsx +441 -98
  10. package/cloud-os/chat/code-runner.tsx +106 -0
  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/range.ts +8 -0
  15. package/cloud-os/chat/rich-blocks.tsx +285 -227
  16. package/cloud-os/chat/surfaces.tsx +90 -0
  17. package/cloud-os/chat/tool-call.tsx +94 -0
  18. package/cloud-os/chat/tool-presentation.ts +31 -22
  19. package/cloud-os/chat/tool-rows.tsx +195 -143
  20. package/cloud-os/chat/tool-timeline.tsx +123 -0
  21. package/cloud-os/chat/transcript-model.ts +258 -20
  22. package/cloud-os/chat/web-search.tsx +135 -0
  23. package/cloud-os/composer/cloud-os-chat-input.tsx +29 -88
  24. package/cloud-os/composer/cloud-os-model-select.tsx +105 -0
  25. package/cloud-os/file-view.tsx +195 -12
  26. package/cloud-os/index.ts +24 -3
  27. package/cloud-os/layout/cloud-os-workspace-split.tsx +60 -3
  28. package/cloud-os/primitives/collapsible.tsx +21 -0
  29. package/cloud-os/primitives/workshop-controls.tsx +2 -2
  30. package/cloud-os/styles/cloud-os.css +114 -1
  31. package/demo/camel-chat-showcase.tsx +21 -13
  32. package/demo/chat-scenarios.ts +214 -40
  33. package/demo/cloud-os-chat-showcase.tsx +37 -14
  34. package/demo/fixtures.ts +4 -5
  35. package/demo/message-panel-gallery.tsx +16 -2
  36. package/package.json +8 -4
  37. package/springbrand-pet/LICENSE.bloub +21 -0
  38. package/springbrand-pet/bot/cycles.test.ts +221 -0
  39. package/springbrand-pet/bot/cycles.ts +225 -0
  40. package/springbrand-pet/bot/decor.ts +285 -0
  41. package/springbrand-pet/bot/engine.test.ts +543 -0
  42. package/springbrand-pet/bot/engine.ts +558 -0
  43. package/springbrand-pet/bot/expressions.test.ts +135 -0
  44. package/springbrand-pet/bot/expressions.ts +192 -0
  45. package/springbrand-pet/bot/eyefit.ts +455 -0
  46. package/springbrand-pet/bot/face.test.ts +101 -0
  47. package/springbrand-pet/bot/face.ts +179 -0
  48. package/springbrand-pet/bot/math.ts +42 -0
  49. package/springbrand-pet/bot/profiles.ts +22 -0
  50. package/springbrand-pet/bot/repere.ts +31 -0
  51. package/springbrand-pet/bot/shape.test.ts +97 -0
  52. package/springbrand-pet/bot/shape.ts +310 -0
  53. package/springbrand-pet/bot/skins.test.ts +325 -0
  54. package/springbrand-pet/bot/skins.ts +161 -0
  55. package/springbrand-pet/bot/states.ts +616 -0
  56. package/springbrand-pet/drag.test.ts +19 -0
  57. package/springbrand-pet/index.tsx +569 -0
  58. package/springbrand-pet/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +1 -0
  59. package/springbrand-pet/vitest.config.ts +9 -0
  60. package/src/camel/camel-chat-messages.tsx +78 -78
  61. package/src/camel/camel-tool-presentation.tsx +19 -15
  62. package/src/camel/camel-turn.ts +1 -17
  63. package/src/composer/composer-attachments.tsx +1 -1
  64. package/src/composer/composer.tsx +2 -2
  65. package/src/contracts.ts +0 -2
  66. package/src/message-panel.tsx +1 -24
  67. package/src/parts/index.tsx +103 -64
@@ -2,6 +2,7 @@ import {
2
2
  Bell,
3
3
  CaretRight,
4
4
  Check,
5
+ CheckCircle,
5
6
  CircleNotch,
6
7
  Clock,
7
8
  Key,
@@ -10,10 +11,20 @@ import {
10
11
  WarningCircle,
11
12
  } from "@phosphor-icons/react";
12
13
  import { useState } from "react";
14
+ import {
15
+ Collapsible,
16
+ CollapsibleContent,
17
+ CollapsibleTrigger,
18
+ } from "../primitives/collapsible";
13
19
  import { Tooltip } from "../primitives/tooltip";
14
20
  import { WorkshopButton } from "../primitives/workshop-controls";
15
21
  import { MarkdownMessage } from "./markdown-message";
16
- import { WorkIcon } from "./tool-rows";
22
+ import { collapsePanel } from "./surfaces";
23
+ import {
24
+ DISCLOSURE_ICON,
25
+ DISCLOSURE_TITLE_WITH_ICON,
26
+ WorkIcon,
27
+ } from "./tool-rows";
17
28
  import type {
18
29
  ParallelTool,
19
30
  PlanStep,
@@ -29,7 +40,7 @@ import {
29
40
  /**
30
41
  * UIMessage 协议里存在、gadgets 那套没有的几类 part —— 计划快照、原位提问、
31
42
  * 后续建议、定时任务、并行/子 Agent、原位授权。视觉语言全部沿用 cloud-os:
32
- * 折叠工作行用 `px-1.5 py-1` 的行式布局,卡片用
43
+ * 折叠工作行用 `py-1` 的行式布局,卡片用
33
44
  * `themed-surface-inset rounded-2xl border-kumo-line/70 bg-kumo-elevated/45 p-3`,
34
45
  * 授权卡沿用 renderActionCard 的 blocking callout(brand/40 边 + brand/10 底)。
35
46
  */
@@ -39,73 +50,78 @@ import {
39
50
  export function PlanBlock({
40
51
  steps,
41
52
  running,
53
+ defaultOpen = true,
42
54
  }: {
43
55
  steps: PlanStep[];
44
56
  running: boolean;
57
+ defaultOpen?: boolean;
45
58
  }) {
46
59
  const done = steps.filter((step) => step.status === "done").length;
47
60
  const current = steps.find((step) => step.status === "in_progress");
48
- const [open, setOpen] = useState(true);
61
+ const [open, setOpen] = useState(defaultOpen);
49
62
 
50
63
  return (
51
- <div className="group -ml-0.5">
52
- <button
53
- type="button"
54
- onClick={() => setOpen((value) => !value)}
55
- className="flex w-full cursor-pointer items-center gap-3 rounded-xl px-1.5 py-1 text-left text-kumo-subtle transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none active:scale-[0.995]"
56
- aria-expanded={open}
64
+ <Collapsible open={open} onOpenChange={setOpen} className="group">
65
+ <CollapsibleTrigger
66
+ className={`${DISCLOSURE_TITLE_WITH_ICON} w-full`}
57
67
  >
58
- <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
59
- <WorkIcon Icon={ListChecks} />
68
+ <span
69
+ className={`${DISCLOSURE_ICON} rounded-full`}
70
+ role="progressbar"
71
+ aria-label="Plan progress"
72
+ aria-valuemin={0}
73
+ aria-valuemax={steps.length}
74
+ aria-valuenow={done}
75
+ style={{
76
+ background: `conic-gradient(var(--color-kumo-brand) ${(done / steps.length) * 100}%, var(--color-kumo-fill) 0)`,
77
+ }}
78
+ >
79
+ <span className="size-2.5 rounded-full bg-kumo-base" />
60
80
  </span>
61
- <span className="min-w-0 flex-1">
62
- <span className="flex min-w-0 items-center gap-2 text-[14px] leading-5 tracking-[-0.25px]">
63
- <span className={`min-w-0 truncate ${running ? "cos-thinking-shimmer" : ""}`}>
64
- Plan · {done}/{steps.length}
65
- </span>
66
- <CaretRight
67
- size={13}
68
- weight="bold"
69
- className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
70
- />
81
+ <span className="flex min-w-0 flex-1 items-center gap-2">
82
+ <span className={`shrink-0 ${running ? "cos-thinking-shimmer" : ""}`}>
83
+ Plan · {done}/{steps.length}
71
84
  </span>
72
85
  {!open && current && (
73
- <span className="mt-1 block truncate font-mono text-[12px] leading-4 text-kumo-inactive">
86
+ <span className="min-w-0 flex-1 truncate text-[12px] leading-4 text-[rgba(0,0,0,0.3)]">
74
87
  {current.text}
75
88
  </span>
76
89
  )}
90
+ <CaretRight
91
+ size={16}
92
+ className={`ml-auto flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${open ? "rotate-90" : ""}`}
93
+ />
77
94
  </span>
78
- </button>
79
- {open && (
80
- <div className="themed-surface-inset ml-8 mt-1 space-y-1.5 rounded-2xl border border-kumo-line/70 bg-kumo-elevated/45 p-3">
95
+ </CollapsibleTrigger>
96
+ <CollapsibleContent className={collapsePanel}>
97
+ <div className="-mx-2.5 mt-1 flex flex-col gap-2 rounded-xl border border-[rgba(0,0,0,0.07)] px-4 py-3">
81
98
  {steps.map((step, index) => (
82
99
  <div
83
100
  key={`${step.text}-${index}`}
84
- className="flex items-start gap-2.5 text-[13px] leading-[19px] tracking-[-0.25px]"
101
+ className="flex items-center gap-2 text-[14px] leading-[1.4] font-normal tracking-normal"
85
102
  >
86
103
  <span
87
- className="mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center"
104
+ className="flex size-4 shrink-0 items-center justify-center"
88
105
  aria-hidden="true"
89
106
  >
90
107
  {step.status === "done" ? (
91
- <Check size={12} weight="bold" className="text-kumo-success" />
108
+ <CheckCircle size={16} weight="fill" className="text-[#00bf75]" />
92
109
  ) : step.status === "in_progress" ? (
93
110
  <CircleNotch
94
- size={12}
95
- weight="bold"
96
- className="animate-spin text-kumo-brand motion-reduce:animate-none"
111
+ size={16}
112
+ className="animate-spin text-[#ff9938] motion-reduce:animate-none"
97
113
  />
98
114
  ) : (
99
- <span className="h-1.5 w-1.5 rounded-full bg-kumo-fill" />
115
+ <span className="h-1.5 w-1.5 rounded-full bg-[color:var(--text-color-kumo-default)] opacity-20" />
100
116
  )}
101
117
  </span>
102
118
  <span
103
119
  className={
104
120
  step.status === "done"
105
- ? "min-w-0 flex-1 text-kumo-inactive line-through"
121
+ ? "min-w-0 flex-1 text-kumo-default/30 line-through"
106
122
  : step.status === "in_progress"
107
- ? "min-w-0 flex-1 font-medium text-kumo-default"
108
- : "min-w-0 flex-1 text-kumo-subtle"
123
+ ? "min-w-0 flex-1 text-kumo-default"
124
+ : "min-w-0 flex-1 text-kumo-default/70"
109
125
  }
110
126
  >
111
127
  {step.text}
@@ -113,8 +129,8 @@ export function PlanBlock({
113
129
  </div>
114
130
  ))}
115
131
  </div>
116
- )}
117
- </div>
132
+ </CollapsibleContent>
133
+ </Collapsible>
118
134
  );
119
135
  }
120
136
 
@@ -124,33 +140,32 @@ interface AskQuestion {
124
140
  prompt: string;
125
141
  options: string[];
126
142
  multi: boolean;
143
+ allowCustom: boolean;
127
144
  }
128
145
 
129
146
  function questionsOf(input: Record<string, unknown>): AskQuestion[] {
130
- if (Array.isArray(input.questions)) {
131
- return input.questions.map((item) => {
132
- const question = (item ?? {}) as Record<string, unknown>;
133
- return {
134
- prompt: String(question.prompt ?? question.question ?? "Choose an option"),
135
- options: Array.isArray(question.options)
136
- ? question.options.filter((o): o is string => typeof o === "string")
137
- : [],
138
- multi: question.kind === "multi" || question.multiSelect === true,
139
- };
140
- });
141
- }
142
- if (typeof input.question === "string") {
143
- return [
144
- {
145
- prompt: input.question,
146
- options: Array.isArray(input.options)
147
- ? input.options.filter((o): o is string => typeof o === "string")
148
- : [],
149
- multi: input.multiSelect === true,
150
- },
151
- ];
152
- }
153
- return [];
147
+ if (!Array.isArray(input.questions)) return [];
148
+ return input.questions.map((item) => {
149
+ const question = (item ?? {}) as Record<string, unknown>;
150
+ const options = Array.isArray(question.options)
151
+ ? question.options.filter((option): option is string =>
152
+ typeof option === "string"
153
+ )
154
+ : [];
155
+ return {
156
+ prompt: typeof question.question === "string"
157
+ ? question.question
158
+ : "Choose an option",
159
+ options,
160
+ multi: question.multiSelect === true,
161
+ allowCustom: question.allowCustom === true || options.length === 0,
162
+ };
163
+ });
164
+ }
165
+
166
+ interface DraftAnswer {
167
+ selections: string[];
168
+ text: string;
154
169
  }
155
170
 
156
171
  export function AskUserBlock({
@@ -162,124 +177,171 @@ export function AskUserBlock({
162
177
  onRespond?: (toolCallId: string, response: unknown) => Promise<boolean>;
163
178
  }) {
164
179
  const questions = questionsOf(call.input);
165
- // 不能用 `useState(() => questions.map(() => []))` 初始化:工具入参是**流式**到达的,
166
- // 首帧 questions 往往还是空数组,那之后每次 setSelections 都在空数组上 map,
180
+ // 不能用 `useState(() => questions.map(...))` 初始化:工具入参是**流式**到达的,
181
+ // 首帧 questions 往往还是空数组,那之后每次 setAnswers 都在空数组上 map,
167
182
  // 选择永远存不进去 → Submit 永久禁用。改成稀疏存 + 按当前 questions 补齐。
168
- const [selections, setSelections] = useState<string[][]>([]);
169
- const selectionAt = (index: number) => selections[index] ?? [];
183
+ const [answers, setAnswers] = useState<DraftAnswer[]>([]);
184
+ const answerAt = (index: number): DraftAnswer =>
185
+ answers[index] ?? { selections: [], text: "" };
170
186
  const toggle = (questionIndex: number, option: string, multi: boolean) =>
171
- setSelections((current) => {
172
- const next = questions.map((_, index) => current[index] ?? []);
173
- const values = next[questionIndex] ?? [];
174
- next[questionIndex] = !multi
175
- ? [option]
176
- : values.includes(option)
177
- ? values.filter((value) => value !== option)
178
- : [...values, option];
187
+ setAnswers((current) => {
188
+ const next = questions.map((_, index) =>
189
+ current[index] ?? { selections: [], text: "" }
190
+ );
191
+ const answer = next[questionIndex]!;
192
+ next[questionIndex] = {
193
+ ...answer,
194
+ selections: !multi
195
+ ? [option]
196
+ : answer.selections.includes(option)
197
+ ? answer.selections.filter((value) => value !== option)
198
+ : [...answer.selections, option],
199
+ };
200
+ return next;
201
+ });
202
+ const setText = (questionIndex: number, text: string) =>
203
+ setAnswers((current) => {
204
+ const next = questions.map((_, index) =>
205
+ current[index] ?? { selections: [], text: "" }
206
+ );
207
+ next[questionIndex] = { ...next[questionIndex]!, text };
179
208
  return next;
180
209
  });
181
210
  // 在途只用于按钮转圈;权威状态永远是 part 的 state。
182
211
  const [inFlight, setInFlight] = useState(false);
183
212
  const outcome = askUserOutcome(call);
184
- const answer = outcome.kind === "answered" ? outcome : null;
185
- const answeredSelections = new Set(answer?.selections ?? []);
186
- const answeredText = [
187
- ...(answer?.selections ?? []),
188
- ...(answer?.text.trim() ? [answer.text.trim()] : []),
189
- ].join(" / ");
213
+ const displayAnswerAt = (index: number): DraftAnswer => {
214
+ if (outcome.kind !== "answered") return answerAt(index);
215
+ const answer = outcome.answers[index];
216
+ return {
217
+ selections: answer?.selections ?? [],
218
+ text: answer?.text ?? "",
219
+ };
220
+ };
190
221
  // 服务端已经结算 = 这张卡永久不可操作,不管结算成什么。
191
222
  const resolved = outcome.kind !== "pending" || inFlight;
192
223
  const valid = questions.length > 0 && questions.every(
193
- (question, index) =>
194
- question.options.length === 0 || selectionAt(index).length > 0,
224
+ (_question, index) =>
225
+ answerAt(index).selections.length > 0 || answerAt(index).text.trim(),
195
226
  );
196
- const buttonLabel = outcome.kind === "answered"
197
- ? "Submitted"
198
- : outcome.kind === "closed"
199
- ? "Closed"
200
- : inFlight
201
- ? "Submitting…"
202
- : "Submit";
203
- const footnote = outcome.kind === "answered"
204
- ? answeredText && `Answered: ${answeredText}`
205
- : outcome.kind === "closed"
227
+ const buttonLabel = inFlight ? "Submitting…" : "Submit";
228
+ const footnote = outcome.kind === "closed"
206
229
  ? outcome.reason === "user_replied_freeform"
207
230
  ? "Answered in the chat instead."
208
231
  : "Closed without an answer."
209
232
  : "";
210
233
 
211
- // 提交按钮固定在卡片右下角,和脚注同一行 —— 脚注不再单独占一行,多问题时
212
- // 按钮也不会卡在某一题的选项中间。选项没到(流式首帧 questions 为空)时这行
213
- // 照样渲染,否则用户看到的是一张空卡。
214
- const submit = (
215
- <WorkshopButton
216
- tone="primary"
217
- disabled={resolved || !valid || !onRespond}
218
- onClick={() => {
219
- if (!onRespond) return;
220
- setInFlight(true);
221
- // 失败就把按钮放回可点:权威状态由 part 决定,这里只管别把用户锁死。
222
- void onRespond(call.toolCallId, {
223
- selections: questions.flatMap((_, index) => selectionAt(index)),
224
- text: "",
225
- }).then((ok) => {
226
- if (!ok) setInFlight(false);
227
- });
228
- }}
229
- className="ml-auto !h-7 flex-shrink-0 gap-1 !rounded-lg px-2.5 text-[12px]"
230
- >
231
- {buttonLabel}
232
- </WorkshopButton>
233
- );
234
+ const submit = () => {
235
+ if (!onRespond) return;
236
+ setInFlight(true);
237
+ // 失败就把按钮放回可点:权威状态由 part 决定,这里只管别把用户锁死。
238
+ void onRespond(call.toolCallId, {
239
+ answers: questions.map((_, index) => ({
240
+ selections: answerAt(index).selections,
241
+ text: answerAt(index).text.trim(),
242
+ })),
243
+ }).then((ok) => {
244
+ if (!ok) setInFlight(false);
245
+ }, () => setInFlight(false));
246
+ };
234
247
 
235
248
  return (
236
- // 收成 w-fit:短问题不该撑成一条横贯全栏的长条,长问题到 560px 再换行。
237
- <div className="w-fit min-w-[15rem] max-w-[560px]">
238
- {/* 组内(问题↔选项)10px、组间(题与题、题与按钮行)12px —— 组间必须比组内
239
- 松,否则多问题时上一题的选项会看起来像下一题的。
240
- 用 flex+gap 而不是 space-y-*:Tailwind v4 的 space-y 选择器裹在
241
- `:where()` 里,特异性被抹平,会被子元素自己的 `m-0` 压掉 → 不生效。 */}
242
- <div className="flex flex-col gap-3 rounded-xl border border-kumo-line bg-kumo-base px-3 py-2.5">
243
- {questions.map((question, questionIndex) => (
244
- <div
245
- key={`${question.prompt}:${questionIndex}`}
246
- className="flex flex-col gap-2.5"
247
- >
248
- <p className="m-0 text-[13px] leading-[18px] font-medium tracking-[-0.25px] text-kumo-default">
249
- {question.prompt}
250
- </p>
251
- <div className="flex flex-wrap items-center gap-1.5">
252
- {question.options.map((option) => {
253
- const selected = answer
254
- ? answeredSelections.has(option)
255
- : selectionAt(questionIndex).includes(option);
256
- return (
257
- <button
258
- key={option}
259
- type="button"
249
+ <div
250
+ className="w-full"
251
+ data-part-type="ask_user"
252
+ data-state={outcome.kind === "answered" ? "submitted" : outcome.kind}
253
+ >
254
+ <div className="flex flex-col gap-4 py-2.5">
255
+ {outcome.kind !== "closed" && questions.map((question, questionIndex) => {
256
+ const answer = displayAnswerAt(questionIndex);
257
+ const options = outcome.kind === "answered"
258
+ ? [...new Set([...question.options, ...answer.selections])]
259
+ : question.options;
260
+ return (
261
+ <div
262
+ key={`${question.prompt}:${questionIndex}`}
263
+ className="flex flex-col items-start gap-2"
264
+ >
265
+ <p className="m-0 text-[14px] leading-[1.4] font-normal text-kumo-default">
266
+ {question.prompt}
267
+ </p>
268
+ {options.length > 0 && (
269
+ <div className="flex flex-wrap items-center gap-2">
270
+ {options.map((option) => {
271
+ const selected = answer.selections.includes(option);
272
+ return (
273
+ <button
274
+ key={option}
275
+ type="button"
276
+ aria-pressed={selected}
277
+ disabled={resolved || !onRespond}
278
+ onClick={() =>
279
+ toggle(questionIndex, option, question.multi)}
280
+ style={selected
281
+ ? {
282
+ background:
283
+ "linear-gradient(#fff,#fff) padding-box, linear-gradient(90deg,#ff9938,#ffe46c) border-box",
284
+ }
285
+ : undefined}
286
+ className={`inline-flex w-fit items-center justify-center rounded-lg border px-4 py-2 text-left text-[14px] leading-[1.4] font-normal text-[rgba(0,0,0,0.7)] transition-[background,border-color] duration-150 ease-out focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ff9938]/30 ${
287
+ selected
288
+ ? "border-solid border-transparent"
289
+ : resolved || !onRespond
290
+ ? "border-[rgba(0,0,0,0.07)] bg-kumo-base opacity-60"
291
+ : "cursor-pointer border-[rgba(0,0,0,0.07)] bg-kumo-base hover:border-[#ff9938]"
292
+ }`}
293
+ >
294
+ {option}
295
+ </button>
296
+ );
297
+ })}
298
+ </div>
299
+ )}
300
+ {question.allowCustom && (
301
+ <label className="flex w-full flex-col gap-2 text-[14px] leading-[1.4] text-kumo-default">
302
+ <span>{question.options.length > 0 ? "Other" : "Answer"}</span>
303
+ <input
304
+ type="text"
305
+ value={answer.text}
306
+ maxLength={2_000}
260
307
  disabled={resolved || !onRespond}
261
- onClick={() => toggle(questionIndex, option, question.multi)}
262
- className={`inline-flex h-7 cursor-pointer items-center rounded-lg border px-2.5 text-[13px] leading-none tracking-[-0.25px] transition-colors duration-150 ease-out disabled:cursor-default ${
263
- selected
264
- ? "border-kumo-brand/45 bg-kumo-brand/10 text-kumo-default"
265
- : "border-kumo-line bg-kumo-base text-kumo-subtle hover:bg-kumo-tint hover:text-kumo-default"
266
- }`}
267
- >
268
- {option}
269
- </button>
270
- );
271
- })}
308
+ aria-label={`自定义回答:${question.prompt}`}
309
+ placeholder={question.options.length > 0
310
+ ? "Add a custom answer"
311
+ : "Type your answer"}
312
+ onChange={(event) =>
313
+ setText(questionIndex, event.currentTarget.value)}
314
+ className="rounded-lg border border-[rgba(0,0,0,0.07)] bg-kumo-base px-4 py-2 text-[14px] leading-[1.4] text-kumo-subtle outline-none placeholder:text-kumo-inactive focus:border-[#ff9938] focus:ring-2 focus:ring-[#ff9938]/20 disabled:cursor-default disabled:opacity-60"
315
+ />
316
+ </label>
317
+ )}
318
+ {outcome.kind === "answered" &&
319
+ !question.allowCustom && answer.text.trim() && (
320
+ <p className="m-0 text-[14px] leading-[1.4] text-kumo-subtle">
321
+ {answer.text.trim()}
322
+ </p>
323
+ )}
272
324
  </div>
273
- </div>
274
- ))}
275
- <div className="flex items-center gap-3">
276
- {footnote && (
277
- <p className="m-0 min-w-0 text-[12px] leading-4 text-kumo-inactive">
278
- {footnote}
279
- </p>
280
- )}
281
- {submit}
282
- </div>
325
+ );
326
+ })}
327
+ {outcome.kind === "pending" && (
328
+ <button
329
+ type="button"
330
+ disabled={resolved || !onRespond || !valid}
331
+ onClick={submit}
332
+ className="inline-flex h-10 min-w-[120px] self-start items-center justify-center rounded-lg bg-gradient-to-r from-[#ff9938] to-[#ffe46c] px-5 py-2.5 text-[14px] leading-5 font-medium text-black transition-opacity duration-150 ease-out hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#ff9938]/30 disabled:cursor-default disabled:opacity-50"
333
+ >
334
+ {buttonLabel}
335
+ </button>
336
+ )}
337
+ {outcome.kind === "answered" && (
338
+ <p className="m-0 text-[12px] leading-4 text-kumo-inactive">Submitted</p>
339
+ )}
340
+ {outcome.kind === "closed" && (
341
+ <p className="m-0 text-[12px] leading-4 text-kumo-inactive">
342
+ Closed{footnote ? ` · ${footnote}` : ""}
343
+ </p>
344
+ )}
283
345
  </div>
284
346
  </div>
285
347
  );
@@ -295,23 +357,26 @@ export function SuggestionsBlock({
295
357
  onSuggestion: (text: string) => void;
296
358
  }) {
297
359
  return (
298
- <div className="max-w-[860px] space-y-2 pt-1">
299
- <div className="flex items-center gap-2 px-1.5 text-[13px] leading-[18px] text-kumo-success">
300
- <Check size={14} weight="bold" />
301
- <span>Task complete</span>
302
- </div>
303
- <div className="flex flex-wrap gap-1.5 px-1.5">
304
- {items.map((item) => (
305
- <button
306
- key={item}
307
- type="button"
308
- onClick={() => onSuggestion(item)}
309
- className="inline-flex h-7 cursor-pointer items-center rounded-full border border-kumo-line bg-kumo-base px-3 text-[13px] leading-none tracking-[-0.25px] text-kumo-subtle transition-colors duration-150 ease-out hover:bg-kumo-tint hover:text-kumo-default focus-visible:outline-none active:scale-[0.98]"
310
- >
311
- {item}
312
- </button>
313
- ))}
314
- </div>
360
+ <div className="flex max-w-[860px] flex-col items-start gap-3">
361
+ {items.map((item) => (
362
+ <button
363
+ key={item}
364
+ type="button"
365
+ onClick={() => onSuggestion(item)}
366
+ className="inline-flex cursor-pointer items-center gap-2 text-[14px] leading-[1.4] text-kumo-inactive transition-colors duration-150 ease-out hover:text-kumo-brand focus-visible:text-kumo-brand focus-visible:outline-none active:scale-[0.98]"
367
+ >
368
+ <span className="grid size-5 flex-shrink-0 place-items-center" aria-hidden="true">
369
+ <span className="rotate-180 -scale-y-100">
370
+ <img
371
+ src={new URL("../assets/followup-arrow.svg", import.meta.url).href}
372
+ alt=""
373
+ className="h-[8.33333px] w-[11.6667px]"
374
+ />
375
+ </span>
376
+ </span>
377
+ <span className="whitespace-nowrap">{item}</span>
378
+ </button>
379
+ ))}
315
380
  </div>
316
381
  );
317
382
  }
@@ -356,7 +421,7 @@ function scheduleCadence(input: Record<string, unknown>): string {
356
421
  return timezone ? `${cadence} · ${timezone}` : cadence;
357
422
  }
358
423
 
359
- /** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id 即任务在 SchedulerAgent 里的主键。 */
424
+ /** `schedule` 工具的 details 是 `{ scheduled: true, id }`,id Inbox Schedule Definition 主键。 */
360
425
  function scheduleIdOf(output: unknown): string {
361
426
  if (typeof output !== "object" || output === null) return "";
362
427
  const id = (output as Record<string, unknown>).id;
@@ -382,7 +447,7 @@ export function ScheduleConfirmation({
382
447
  return (
383
448
  <section
384
449
  data-slot="schedule-card"
385
- aria-label={`确认定时任务:${name}`}
450
+ aria-label={`Confirm scheduled task: ${name}`}
386
451
  className="themed-surface-inset flex w-full max-w-sm flex-col gap-3 rounded-2xl border border-kumo-line/70 bg-kumo-elevated p-4 text-kumo-default shadow-[0_1px_2px_rgba(0,0,0,0.04),0_12px_32px_-16px_rgba(0,0,0,0.12)]"
387
452
  >
388
453
  <div className="flex items-center gap-2.5">
@@ -413,10 +478,10 @@ export function ScheduleConfirmation({
413
478
 
414
479
  <div className="flex items-center justify-end gap-2">
415
480
  <WorkshopButton disabled={disabled} onClick={onCancel}>
416
- 取消
481
+ Cancel
417
482
  </WorkshopButton>
418
483
  <WorkshopButton tone="primary" disabled={disabled} onClick={onConfirm}>
419
- {disabled ? "正在确认…" : "确认安排"}
484
+ {disabled ? "Confirming…" : "Confirm schedule"}
420
485
  </WorkshopButton>
421
486
  </div>
422
487
  </section>
@@ -520,22 +585,22 @@ export function PermissionGrant({
520
585
  "aria-label"?: string;
521
586
  }) {
522
587
  const actionClassName =
523
- "inline-flex h-8 items-center rounded-full px-3 text-[12px] leading-none font-medium tracking-[-0.25px] transition-[background-color,color,opacity,transform] duration-150 ease-out active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-ring/40 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100";
588
+ "inline-flex h-10 w-[120px] items-center justify-center rounded-lg border border-kumo-line px-4 text-[14px] leading-[1.4] font-medium transition-[background-color,color,opacity,transform] duration-150 ease-out active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-ring/40 disabled:cursor-not-allowed disabled:opacity-40 disabled:active:scale-100";
524
589
 
525
590
  return (
526
591
  <section
527
592
  aria-label={ariaLabel}
528
- className="themed-surface-inset w-full max-w-sm rounded-[20px] border border-kumo-line bg-kumo-elevated p-4 text-kumo-default shadow-sm"
593
+ className="themed-surface-inset w-full rounded-xl border border-kumo-line bg-kumo-elevated p-4 text-kumo-default"
529
594
  >
530
- <div className="flex items-center gap-2.5">
595
+ <div className="flex items-center gap-2">
531
596
  <span
532
- className="flex size-7 shrink-0 items-center justify-center rounded-lg bg-kumo-fill/70 text-kumo-subtle"
597
+ className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-kumo-fill text-kumo-subtle"
533
598
  aria-hidden="true"
534
599
  >
535
600
  <Key size={14} weight="bold" />
536
601
  </span>
537
602
  <div className="flex min-w-0 flex-1 flex-col">
538
- <span className="truncate text-[13.5px] leading-[18px] font-medium">
603
+ <span className="truncate text-[14px] leading-[1.4] font-medium">
539
604
  {capability}
540
605
  </span>
541
606
  <span className="truncate text-[12px] leading-4 text-kumo-inactive">
@@ -544,41 +609,22 @@ export function PermissionGrant({
544
609
  </div>
545
610
  </div>
546
611
 
547
- <div className="mt-3.5 flex max-h-[200px] flex-col gap-1 overflow-y-auto pr-1">
548
- <span className="font-mono text-[10px] leading-4 text-kumo-inactive">
612
+ <div className="mt-4 flex max-h-[200px] flex-col gap-2 overflow-y-auto">
613
+ <span className="text-[12px] leading-4 text-kumo-inactive">
549
614
  this grants
550
615
  </span>
551
616
  {reach.map((item, index) => (
552
617
  <span
553
618
  key={`${index}:${item}`}
554
- className="flex items-baseline gap-2 text-[12px] leading-[17px] text-kumo-subtle"
619
+ className="flex items-baseline gap-2 text-[14px] leading-[1.4] text-kumo-subtle"
555
620
  >
556
- <span
557
- className="size-1 shrink-0 rounded-full bg-kumo-interact"
558
- aria-hidden="true"
559
- />
621
+ <span aria-hidden="true">•</span>
560
622
  <span className="min-w-0 break-words whitespace-pre-wrap">{item}</span>
561
623
  </span>
562
624
  ))}
563
625
  </div>
564
626
 
565
- <div className="mt-3.5 flex min-h-8 flex-wrap items-center justify-end gap-1">
566
- <button
567
- type="button"
568
- disabled={disabled}
569
- onClick={() => onGrant("deny")}
570
- className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
571
- >
572
- Deny
573
- </button>
574
- <button
575
- type="button"
576
- disabled={disabled}
577
- onClick={() => onGrant("allow_once")}
578
- className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
579
- >
580
- Allow once
581
- </button>
627
+ <div className="mt-4 flex flex-wrap items-center gap-2">
582
628
  <Tooltip
583
629
  content="Allow every action at this execution level, without future prompts."
584
630
  asChild
@@ -587,11 +633,27 @@ export function PermissionGrant({
587
633
  type="button"
588
634
  disabled={disabled}
589
635
  onClick={() => onGrant("allow_level")}
590
- className={`${actionClassName} bg-kumo-contrast text-kumo-inverse enabled:hover:bg-kumo-strong`}
636
+ className={`${actionClassName} border-0 bg-[linear-gradient(90deg,#ffd077_0%,#ff9938_100%)] text-kumo-inverse enabled:hover:brightness-95`}
591
637
  >
592
638
  Allow level
593
639
  </button>
594
640
  </Tooltip>
641
+ <button
642
+ type="button"
643
+ disabled={disabled}
644
+ onClick={() => onGrant("allow_once")}
645
+ className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
646
+ >
647
+ Allow once
648
+ </button>
649
+ <button
650
+ type="button"
651
+ disabled={disabled}
652
+ onClick={() => onGrant("deny")}
653
+ className={`${actionClassName} text-kumo-subtle enabled:hover:bg-kumo-fill enabled:hover:text-kumo-default`}
654
+ >
655
+ Deny
656
+ </button>
595
657
  </div>
596
658
  </section>
597
659
  );
@@ -659,15 +721,15 @@ export function ParallelBlock({
659
721
  const done = tools.filter((tool) => tool.status === "done").length;
660
722
  return (
661
723
  <div className="max-w-[860px]">
662
- <div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
663
- <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
724
+ <div className="flex items-start gap-2 px-1.5 py-1 text-[14px] leading-[1.4] text-kumo-inactive">
725
+ <span className={DISCLOSURE_ICON}>
664
726
  <WorkIcon Icon={ListChecks} />
665
727
  </span>
666
728
  <span className="min-w-0 truncate">
667
729
  {label} · {done}/{tools.length}
668
730
  </span>
669
731
  </div>
670
- <div className="ml-8 mt-1 space-y-1">
732
+ <div className="mt-1 space-y-1">
671
733
  {tools.map((tool, index) => (
672
734
  <div
673
735
  key={`${tool.label}-${index}`}
@@ -690,15 +752,15 @@ export function ParallelBlock({
690
752
  export function SubAgentsBlock({ agents }: { agents: SubAgentView[] }) {
691
753
  return (
692
754
  <div className="max-w-[860px]">
693
- <div className="flex items-center gap-3 px-1.5 py-1 text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
694
- <span className="flex h-5 w-5 flex-shrink-0 items-center justify-center">
755
+ <div className="flex items-start gap-2 px-1.5 py-1 text-[14px] leading-[1.4] text-kumo-inactive">
756
+ <span className={DISCLOSURE_ICON}>
695
757
  <WorkIcon Icon={UsersThree} />
696
758
  </span>
697
759
  <span className="min-w-0 truncate">
698
760
  {agents.length === 1 ? "Sub-agent" : `${agents.length} sub-agents`}
699
761
  </span>
700
762
  </div>
701
- <div className="ml-8 mt-1 space-y-1">
763
+ <div className="mt-1 space-y-1">
702
764
  {agents.map((agent, index) => (
703
765
  <div
704
766
  key={`${agent.name}-${index}`}
@@ -736,28 +798,24 @@ export function ErrorBlock({
736
798
  const [expanded, setExpanded] = useState(false);
737
799
  return (
738
800
  <div className="group/work max-w-[860px] text-[14px] leading-5 tracking-[-0.25px] text-kumo-subtle">
739
- <div className="flex w-full items-center gap-2 px-1.5 py-1">
801
+ <div className="flex w-full items-start gap-2">
740
802
  <button
741
803
  type="button"
742
804
  onClick={() => setExpanded((value) => !value)}
743
- className="flex min-w-0 flex-1 cursor-pointer items-center gap-3 rounded-md text-left transition-colors duration-150 ease-out hover:text-kumo-default focus-visible:text-kumo-default focus-visible:outline-none active:scale-[0.995]"
805
+ className={`${DISCLOSURE_TITLE_WITH_ICON} flex-1`}
744
806
  aria-expanded={expanded}
745
807
  >
746
808
  <span
747
- className="flex h-5 w-5 flex-shrink-0 items-center justify-center text-kumo-danger"
809
+ className={`${DISCLOSURE_ICON} text-kumo-danger`}
748
810
  aria-hidden="true"
749
811
  >
750
- <WarningCircle size={16} weight="fill" />
812
+ <WarningCircle size={15} weight="fill" />
751
813
  </span>
752
814
  <span className="flex min-w-0 flex-1 items-center gap-1">
753
- <span className="min-w-0 truncate">
754
- <span className="font-medium text-kumo-danger">{title}: </span>
755
- <span className="text-kumo-subtle">{body}</span>
756
- </span>
815
+ <span className="min-w-0 truncate text-kumo-danger">{title}</span>
757
816
  {body && (
758
817
  <CaretRight
759
- size={13}
760
- weight="bold"
818
+ size={16}
761
819
  className={`flex-shrink-0 text-kumo-inactive transition-transform duration-150 ease-out ${expanded ? "rotate-90" : ""}`}
762
820
  />
763
821
  )}
@@ -776,7 +834,7 @@ export function ErrorBlock({
776
834
  )}
777
835
  </div>
778
836
  {expanded && body && (
779
- <div className="ml-8 mt-1">
837
+ <div className="mt-1">
780
838
  <pre className="max-h-48 overflow-auto rounded-xl border border-kumo-line/70 bg-kumo-base p-3 font-mono text-[12px] leading-[18px] text-kumo-subtle whitespace-pre-wrap">
781
839
  {body}
782
840
  </pre>