@springbrand/message-panel 0.1.3-alpha.3 → 0.1.3-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cloud-os/README.md +4 -4
- package/cloud-os/capability-chip.tsx +35 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +12 -0
- package/cloud-os/chat/transcript-model.ts +34 -0
- package/cloud-os/composer/cloud-os-chat-input.tsx +173 -22
- package/cloud-os/index.ts +5 -0
- package/cloud-os/layout/cloud-os-workspace-split.tsx +1 -15
- package/cloud-os/styles/cloud-os.css +0 -20
- package/demo/cloud-os-chat-showcase.tsx +0 -1
- package/package.json +1 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
package/cloud-os/README.md
CHANGED
|
@@ -46,10 +46,10 @@ pnpm dev:cloud-os # http://localhost:9992
|
|
|
46
46
|
shadcn 主题正面冲突。
|
|
47
47
|
4. **作用域化主题**。只有 kumo 命名空间的 token 进 `@theme`;通用 token(字体 /
|
|
48
48
|
圆角 / 字号 / 缓动)挂在 `.cloud-os-root` 上靠继承生效,宿主主题分毫不动。
|
|
49
|
-
5. **删掉的东西**:capsule(URL → 资源胶囊)
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
5. **删掉的东西**:capsule(URL → 资源胶囊)、ComposerMirror、GatekeeperModal、
|
|
50
|
+
Yjs 草稿 / 「接受改动」那一整套。它们都直挂 gadgets 的 capnweb RPC,
|
|
51
|
+
UIMessage 协议下没有对应物。命令补全使用 package 内的 assistant-ui
|
|
52
|
+
`ComposerTriggerPopover`,宿主只传命令数据。
|
|
53
53
|
6. **窄栏适配**。授权卡 / 提问卡 / 定时卡在上游是「图标 + 正文 + 右侧动作」一行到底;
|
|
54
54
|
聊天栏默认只有 420px,一行放不下会把正文挤成 0 宽,所以改成允许换行。
|
|
55
55
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Diamond, X } from "@phosphor-icons/react";
|
|
2
|
+
|
|
3
|
+
export interface CapabilityChipProps {
|
|
4
|
+
kind: "skill" | "plan";
|
|
5
|
+
label: string;
|
|
6
|
+
removable?: boolean;
|
|
7
|
+
onRemove?: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function CapabilityChip({
|
|
11
|
+
kind,
|
|
12
|
+
label,
|
|
13
|
+
removable = false,
|
|
14
|
+
onRemove,
|
|
15
|
+
}: CapabilityChipProps) {
|
|
16
|
+
return (
|
|
17
|
+
<span
|
|
18
|
+
data-capability-kind={kind}
|
|
19
|
+
className="inline-flex max-w-full items-center gap-1 rounded-full border border-kumo-line bg-kumo-elevated px-2 py-1 text-[12px] leading-4 text-kumo-subtle"
|
|
20
|
+
>
|
|
21
|
+
<Diamond size={11} weight="duotone" className="shrink-0 text-kumo-brand" />
|
|
22
|
+
<span className="truncate">{label}</span>
|
|
23
|
+
{removable && onRemove && (
|
|
24
|
+
<button
|
|
25
|
+
type="button"
|
|
26
|
+
onClick={onRemove}
|
|
27
|
+
aria-label={`Remove ${label}`}
|
|
28
|
+
className="-mr-1 grid size-5 shrink-0 cursor-pointer place-items-center rounded-full text-kumo-inactive hover:bg-kumo-tint hover:text-kumo-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-brand/40"
|
|
29
|
+
>
|
|
30
|
+
<X size={10} weight="bold" />
|
|
31
|
+
</button>
|
|
32
|
+
)}
|
|
33
|
+
</span>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from "./rich-blocks";
|
|
37
37
|
import { ThinkingTraceRow, ToolGroupRow } from "./tool-rows";
|
|
38
38
|
import { CloudOsActivityIndicator } from "./activity-indicator";
|
|
39
|
+
import { CapabilityChip } from "../capability-chip";
|
|
39
40
|
import {
|
|
40
41
|
buildCloudOsEntries,
|
|
41
42
|
deriveTurnActivity,
|
|
@@ -127,6 +128,17 @@ function UserBubble({
|
|
|
127
128
|
}) {
|
|
128
129
|
return (
|
|
129
130
|
<div className="group/message relative flex flex-col items-end">
|
|
131
|
+
{entry.capabilities.length > 0 && (
|
|
132
|
+
<div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap justify-end gap-1.5">
|
|
133
|
+
{entry.capabilities.map((capability) => (
|
|
134
|
+
<CapabilityChip
|
|
135
|
+
key={`${capability.kind}:${capability.name}`}
|
|
136
|
+
kind={capability.kind}
|
|
137
|
+
label={capability.label}
|
|
138
|
+
/>
|
|
139
|
+
))}
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
130
142
|
{entry.attachments.length > 0 && (
|
|
131
143
|
<div className="mb-1.5 flex max-w-[min(680px,78%)] flex-wrap justify-end gap-2">
|
|
132
144
|
{entry.attachments.map((attachment, index) =>
|
|
@@ -29,6 +29,13 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
|
|
|
29
29
|
turnDurationMs?: number;
|
|
30
30
|
turnStartedAt?: number;
|
|
31
31
|
turnStatus?: string;
|
|
32
|
+
requestedCapabilities?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CloudOsRequestedCapability {
|
|
36
|
+
kind: "skill" | "plan";
|
|
37
|
+
name: string;
|
|
38
|
+
label: string;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
export interface CloudOsAttachment {
|
|
@@ -80,6 +87,7 @@ export type CloudOsEntry =
|
|
|
80
87
|
messageId: string;
|
|
81
88
|
text: string;
|
|
82
89
|
attachments: CloudOsAttachment[];
|
|
90
|
+
capabilities: CloudOsRequestedCapability[];
|
|
83
91
|
authorName?: string;
|
|
84
92
|
timestamp?: number;
|
|
85
93
|
}
|
|
@@ -126,6 +134,31 @@ export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
|
|
|
126
134
|
return recordOf(message.metadata) as CloudOsMessageMetadata;
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
export function requestedCapabilitiesOf(
|
|
138
|
+
metadata: CloudOsMessageMetadata,
|
|
139
|
+
): CloudOsRequestedCapability[] {
|
|
140
|
+
if (!Array.isArray(metadata.requestedCapabilities)) return [];
|
|
141
|
+
const seen = new Set<string>();
|
|
142
|
+
return metadata.requestedCapabilities.flatMap((value) => {
|
|
143
|
+
const capability = recordOf(value);
|
|
144
|
+
if (
|
|
145
|
+
(capability.kind !== "skill" && capability.kind !== "plan") ||
|
|
146
|
+
typeof capability.name !== "string" ||
|
|
147
|
+
!capability.name.trim() ||
|
|
148
|
+
typeof capability.label !== "string" ||
|
|
149
|
+
!capability.label.trim()
|
|
150
|
+
) return [];
|
|
151
|
+
const key = `${capability.kind}:${capability.name}`;
|
|
152
|
+
if (seen.has(key)) return [];
|
|
153
|
+
seen.add(key);
|
|
154
|
+
return [{
|
|
155
|
+
kind: capability.kind,
|
|
156
|
+
name: capability.name,
|
|
157
|
+
label: capability.label,
|
|
158
|
+
}];
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
129
162
|
export function messageText(message: UIMessage): string {
|
|
130
163
|
return message.parts
|
|
131
164
|
.filter(
|
|
@@ -488,6 +521,7 @@ export function buildCloudOsEntries({
|
|
|
488
521
|
messageId: message.id,
|
|
489
522
|
text,
|
|
490
523
|
attachments,
|
|
524
|
+
capabilities: requestedCapabilitiesOf(metadata),
|
|
491
525
|
authorName: metadata.authorDisplayName,
|
|
492
526
|
timestamp:
|
|
493
527
|
nonNegativeNumber(metadata.createdAt) ??
|
|
@@ -8,9 +8,21 @@ import {
|
|
|
8
8
|
Terminal,
|
|
9
9
|
X,
|
|
10
10
|
} from "@phosphor-icons/react";
|
|
11
|
+
import {
|
|
12
|
+
AssistantRuntimeProvider,
|
|
13
|
+
ComposerPrimitive,
|
|
14
|
+
unstable_useSlashCommandAdapter,
|
|
15
|
+
unstable_useTriggerPopoverAriaProps,
|
|
16
|
+
unstable_useTriggerPopoverTriggers,
|
|
17
|
+
useExternalStoreRuntime,
|
|
18
|
+
type AssistantRuntime,
|
|
19
|
+
type ThreadMessage,
|
|
20
|
+
} from "@assistant-ui/react";
|
|
21
|
+
import { ComposerTriggerPopover } from "@springbrand/message-panel/composer";
|
|
11
22
|
import {
|
|
12
23
|
useCallback,
|
|
13
24
|
useEffect,
|
|
25
|
+
useMemo,
|
|
14
26
|
useRef,
|
|
15
27
|
useState,
|
|
16
28
|
type DragEvent as ReactDragEvent,
|
|
@@ -19,6 +31,7 @@ import {
|
|
|
19
31
|
import { DropdownMenu } from "../primitives/dropdown-menu";
|
|
20
32
|
import { Tooltip } from "../primitives/tooltip";
|
|
21
33
|
import { WorkshopIconButton } from "../primitives/workshop-controls";
|
|
34
|
+
import { CapabilityChip } from "../capability-chip";
|
|
22
35
|
|
|
23
36
|
const isComposingKeyEvent = (event: {
|
|
24
37
|
isComposing?: boolean;
|
|
@@ -32,9 +45,10 @@ const isComposingKeyEvent = (event: {
|
|
|
32
45
|
* 附件缩略图行、底部左「+ / 添加资源」右「模型 / 发送-停止」的布局、
|
|
33
46
|
* draftUpdateBanner 插槽、blockedReason 占位文案、Enter 发送 / Shift+Enter 换行。
|
|
34
47
|
*
|
|
35
|
-
* 删掉:capsule(URL → 资源胶囊)、
|
|
36
|
-
*
|
|
37
|
-
*
|
|
48
|
+
* 删掉:capsule(URL → 资源胶囊)、ComposerMirror、GatekeeperModal —— 这三样都直挂
|
|
49
|
+
* gadgets 的 capnweb RPC,在 UIMessage 协议下没有对应物。「添加资源」按钮保留成一个
|
|
50
|
+
* 可选回调,宿主不给就不渲染。Command picker 使用 package 内的
|
|
51
|
+
* assistant-ui ComposerTriggerPopover。
|
|
38
52
|
*/
|
|
39
53
|
|
|
40
54
|
export interface CloudOsModelOption {
|
|
@@ -43,10 +57,15 @@ export interface CloudOsModelOption {
|
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
export interface CloudOsComposerCommand {
|
|
60
|
+
prefix?: "/" | "$";
|
|
46
61
|
name: string;
|
|
47
62
|
description: string;
|
|
48
|
-
|
|
49
|
-
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CloudOsCapabilityView {
|
|
66
|
+
kind: "skill" | "plan";
|
|
67
|
+
name: string;
|
|
68
|
+
label: string;
|
|
50
69
|
}
|
|
51
70
|
|
|
52
71
|
export interface CloudOsAttachmentView {
|
|
@@ -82,11 +101,11 @@ export interface CloudOsChatInputProps {
|
|
|
82
101
|
* 传了就以它为准来决定显示发送键还是停止键。
|
|
83
102
|
*/
|
|
84
103
|
turnActive?: boolean;
|
|
85
|
-
/**
|
|
86
|
-
* 可用的斜杠命令。cloudflare-os 原版是一个直连 RPC 的行内 picker;这里退成
|
|
87
|
-
* 「+」菜单里的一组条目 —— 能力保住,但**没有**边打字边过滤的行内补全。
|
|
88
|
-
*/
|
|
104
|
+
/** 可用的斜杠命令,也会显示在「+」菜单中。 */
|
|
89
105
|
commands?: readonly CloudOsComposerCommand[];
|
|
106
|
+
capabilities?: readonly CloudOsCapabilityView[];
|
|
107
|
+
onCommandSelect?: (command: CloudOsComposerCommand) => void;
|
|
108
|
+
onCapabilityRemove?: (kind: CloudOsCapabilityView["kind"], name: string) => void;
|
|
90
109
|
/** 有值时渲染「添加资源」按钮。 */
|
|
91
110
|
onAttachResource?: () => void;
|
|
92
111
|
attachLabel?: string;
|
|
@@ -106,6 +125,10 @@ export interface CloudOsChatInputProps {
|
|
|
106
125
|
|
|
107
126
|
const MAX_PENDING_ATTACHMENTS = 5;
|
|
108
127
|
|
|
128
|
+
function commandLabel(command: CloudOsComposerCommand): string {
|
|
129
|
+
return `${command.prefix ?? "/"}${command.name.replaceAll("-", " ")}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
109
132
|
function autoResizeTextarea(
|
|
110
133
|
textarea: HTMLTextAreaElement,
|
|
111
134
|
minRows: number,
|
|
@@ -123,7 +146,7 @@ function autoResizeTextarea(
|
|
|
123
146
|
textarea.style.overflow = textarea.scrollHeight > maxH ? "auto" : "hidden";
|
|
124
147
|
}
|
|
125
148
|
|
|
126
|
-
|
|
149
|
+
function CloudOsChatInputContent({
|
|
127
150
|
value,
|
|
128
151
|
onChange,
|
|
129
152
|
onSubmit,
|
|
@@ -141,6 +164,9 @@ export function CloudOsChatInput({
|
|
|
141
164
|
submissionBlocked = false,
|
|
142
165
|
turnActive,
|
|
143
166
|
commands = [],
|
|
167
|
+
capabilities = [],
|
|
168
|
+
onCommandSelect,
|
|
169
|
+
onCapabilityRemove,
|
|
144
170
|
onAttachResource,
|
|
145
171
|
attachLabel,
|
|
146
172
|
showThinkingTraces,
|
|
@@ -152,7 +178,8 @@ export function CloudOsChatInput({
|
|
|
152
178
|
maxRows = 6,
|
|
153
179
|
autoFocus = false,
|
|
154
180
|
footnote,
|
|
155
|
-
|
|
181
|
+
runtime,
|
|
182
|
+
}: CloudOsChatInputProps & { runtime: AssistantRuntime }) {
|
|
156
183
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
157
184
|
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
|
158
185
|
const dragDepthRef = useRef(0);
|
|
@@ -189,6 +216,73 @@ export function CloudOsChatInput({
|
|
|
189
216
|
if (element) autoResizeTextarea(element, minRows, maxRows);
|
|
190
217
|
}, [maxRows, minRows, value]);
|
|
191
218
|
|
|
219
|
+
const selectCommand = useCallback((
|
|
220
|
+
command: CloudOsComposerCommand,
|
|
221
|
+
clearTrigger: boolean,
|
|
222
|
+
) => {
|
|
223
|
+
onCommandSelect?.(command);
|
|
224
|
+
if (clearTrigger) {
|
|
225
|
+
runtime.thread.composer.setText("");
|
|
226
|
+
onChange("");
|
|
227
|
+
}
|
|
228
|
+
requestAnimationFrame(() => textareaRef.current?.focus());
|
|
229
|
+
}, [onChange, onCommandSelect, runtime]);
|
|
230
|
+
|
|
231
|
+
const slashCommands = useMemo(
|
|
232
|
+
() => commands
|
|
233
|
+
.filter((command) => (command.prefix ?? "/") === "/")
|
|
234
|
+
.map((command) => ({
|
|
235
|
+
id: command.name,
|
|
236
|
+
label: commandLabel(command),
|
|
237
|
+
description: command.description,
|
|
238
|
+
execute: () => selectCommand(command, true),
|
|
239
|
+
})),
|
|
240
|
+
[commands, selectCommand],
|
|
241
|
+
);
|
|
242
|
+
const skillCommands = useMemo(
|
|
243
|
+
() => commands
|
|
244
|
+
.filter((command) => command.prefix === "$")
|
|
245
|
+
.map((command) => ({
|
|
246
|
+
id: command.name,
|
|
247
|
+
label: commandLabel(command),
|
|
248
|
+
description: command.description,
|
|
249
|
+
execute: () => selectCommand(command, true),
|
|
250
|
+
})),
|
|
251
|
+
[commands, selectCommand],
|
|
252
|
+
);
|
|
253
|
+
const slashAdapter = unstable_useSlashCommandAdapter({
|
|
254
|
+
commands: slashCommands,
|
|
255
|
+
removeOnExecute: true,
|
|
256
|
+
});
|
|
257
|
+
const skillAdapter = unstable_useSlashCommandAdapter({
|
|
258
|
+
commands: skillCommands,
|
|
259
|
+
removeOnExecute: true,
|
|
260
|
+
});
|
|
261
|
+
// assistant-ui only re-runs a TriggerPopover search when the adapter changes.
|
|
262
|
+
const slash = useMemo(() => ({
|
|
263
|
+
...slashAdapter,
|
|
264
|
+
adapter: { ...slashAdapter.adapter },
|
|
265
|
+
}), [slashAdapter, slashCommands]);
|
|
266
|
+
const skills = useMemo(() => ({
|
|
267
|
+
...skillAdapter,
|
|
268
|
+
adapter: { ...skillAdapter.adapter },
|
|
269
|
+
}), [skillAdapter, skillCommands]);
|
|
270
|
+
const triggers = unstable_useTriggerPopoverTriggers();
|
|
271
|
+
const triggerAria = unstable_useTriggerPopoverAriaProps();
|
|
272
|
+
|
|
273
|
+
const setTriggerCursor = useCallback(
|
|
274
|
+
(position: number) => {
|
|
275
|
+
for (const trigger of triggers.values()) {
|
|
276
|
+
trigger.resource.setCursorPosition(position);
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
[triggers],
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
useEffect(() => {
|
|
283
|
+
setTriggerCursor(textareaRef.current?.selectionStart ?? value.length);
|
|
284
|
+
}, [commands, setTriggerCursor, value]);
|
|
285
|
+
|
|
192
286
|
const addFiles = useCallback(
|
|
193
287
|
(files: readonly File[]) => {
|
|
194
288
|
if (files.length === 0) return;
|
|
@@ -224,6 +318,20 @@ export function CloudOsChatInput({
|
|
|
224
318
|
return (
|
|
225
319
|
// isolation: isolate 把输入框内部的 z-index 关起来,免得盖到 portal 出去的模型下拉。
|
|
226
320
|
<div className="cos-chat-input-root relative isolate px-4 py-4">
|
|
321
|
+
<ComposerTriggerPopover
|
|
322
|
+
char="/"
|
|
323
|
+
{...slash}
|
|
324
|
+
aria-label="Commands"
|
|
325
|
+
emptyItemsLabel="No matching commands"
|
|
326
|
+
className="!w-72 !border-kumo-line/70 !bg-kumo-base !text-kumo-default themed-floating-shadow-lg"
|
|
327
|
+
/>
|
|
328
|
+
<ComposerTriggerPopover
|
|
329
|
+
char="$"
|
|
330
|
+
{...skills}
|
|
331
|
+
aria-label="Skills"
|
|
332
|
+
emptyItemsLabel="No matching skills"
|
|
333
|
+
className="!w-72 !border-kumo-line/70 !bg-kumo-base !text-kumo-default themed-floating-shadow-lg"
|
|
334
|
+
/>
|
|
227
335
|
<input
|
|
228
336
|
ref={attachmentInputRef}
|
|
229
337
|
type="file"
|
|
@@ -276,7 +384,22 @@ export function CloudOsChatInput({
|
|
|
276
384
|
|
|
277
385
|
{banner}
|
|
278
386
|
|
|
279
|
-
<div className="relative px-4 pb-1 pt-3">
|
|
387
|
+
<div className="relative flex flex-nowrap items-center gap-2 px-4 pb-1 pt-3">
|
|
388
|
+
{capabilities.length > 0 && (
|
|
389
|
+
<div className="flex max-w-[65%] shrink-0 flex-nowrap gap-1.5 overflow-x-auto">
|
|
390
|
+
{capabilities.map((capability) => (
|
|
391
|
+
<CapabilityChip
|
|
392
|
+
key={`${capability.kind}:${capability.name}`}
|
|
393
|
+
kind={capability.kind}
|
|
394
|
+
label={capability.label}
|
|
395
|
+
removable
|
|
396
|
+
onRemove={() =>
|
|
397
|
+
onCapabilityRemove?.(capability.kind, capability.name)
|
|
398
|
+
}
|
|
399
|
+
/>
|
|
400
|
+
))}
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
280
403
|
<textarea
|
|
281
404
|
ref={(element) => {
|
|
282
405
|
textareaRef.current = element;
|
|
@@ -294,9 +417,17 @@ export function CloudOsChatInput({
|
|
|
294
417
|
: (placeholder ?? "Ask a follow-up…")
|
|
295
418
|
}
|
|
296
419
|
onChange={(event) => {
|
|
297
|
-
|
|
420
|
+
const next = event.target.value;
|
|
421
|
+
runtime.thread.composer.setText(next);
|
|
422
|
+
onChange(next);
|
|
423
|
+
setTriggerCursor(event.target.selectionStart ?? next.length);
|
|
298
424
|
autoResizeTextarea(event.target, minRows, maxRows);
|
|
299
425
|
}}
|
|
426
|
+
onSelect={(event) =>
|
|
427
|
+
setTriggerCursor(
|
|
428
|
+
event.currentTarget.selectionStart ?? value.length,
|
|
429
|
+
)
|
|
430
|
+
}
|
|
300
431
|
onPaste={(event) => {
|
|
301
432
|
const files = Array.from(event.clipboardData.items)
|
|
302
433
|
.filter((item) => item.kind === "file")
|
|
@@ -308,6 +439,9 @@ export function CloudOsChatInput({
|
|
|
308
439
|
}
|
|
309
440
|
}}
|
|
310
441
|
onKeyDown={(event) => {
|
|
442
|
+
for (const trigger of triggers.values()) {
|
|
443
|
+
if (trigger.resource.handleKeyDown(event)) return;
|
|
444
|
+
}
|
|
311
445
|
// Enter 发送(按住 Shift 换行); IME 组合中不发送
|
|
312
446
|
if (
|
|
313
447
|
event.key === "Enter" &&
|
|
@@ -318,7 +452,8 @@ export function CloudOsChatInput({
|
|
|
318
452
|
if (canSend) onSubmit();
|
|
319
453
|
}
|
|
320
454
|
}}
|
|
321
|
-
className="relative z-[1] w-
|
|
455
|
+
className="relative z-[1] min-w-0 flex-1 resize-none border-none bg-transparent p-0 text-[14px] leading-[22px] tracking-[-0.25px] text-kumo-default outline-none placeholder:text-kumo-inactive disabled:cursor-not-allowed"
|
|
456
|
+
{...triggerAria}
|
|
322
457
|
/>
|
|
323
458
|
</div>
|
|
324
459
|
|
|
@@ -404,20 +539,15 @@ export function CloudOsChatInput({
|
|
|
404
539
|
<>
|
|
405
540
|
{commands.map((command) => (
|
|
406
541
|
<DropdownMenu.Item
|
|
407
|
-
key={command.name}
|
|
408
|
-
onClick={() =>
|
|
409
|
-
onChange(command.prompt);
|
|
410
|
-
requestAnimationFrame(() =>
|
|
411
|
-
textareaRef.current?.focus(),
|
|
412
|
-
);
|
|
413
|
-
}}
|
|
542
|
+
key={`${command.prefix ?? "/"}${command.name}`}
|
|
543
|
+
onClick={() => selectCommand(command, false)}
|
|
414
544
|
className="!h-auto rounded-xl !px-2 !py-1.5 text-[12px] leading-4 font-normal tracking-[-0.15px] text-kumo-subtle transition-colors data-highlighted:bg-kumo-tint/70 data-highlighted:text-kumo-default"
|
|
415
545
|
>
|
|
416
546
|
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
417
547
|
<Terminal size={14} />
|
|
418
548
|
</span>
|
|
419
549
|
<span className="min-w-0 flex-1 truncate">
|
|
420
|
-
|
|
550
|
+
{commandLabel(command)}
|
|
421
551
|
</span>
|
|
422
552
|
</DropdownMenu.Item>
|
|
423
553
|
))}
|
|
@@ -539,3 +669,24 @@ export function CloudOsChatInput({
|
|
|
539
669
|
</div>
|
|
540
670
|
);
|
|
541
671
|
}
|
|
672
|
+
|
|
673
|
+
const EMPTY_MESSAGES: readonly ThreadMessage[] = [];
|
|
674
|
+
|
|
675
|
+
export function CloudOsChatInput(props: CloudOsChatInputProps) {
|
|
676
|
+
const runtime = useExternalStoreRuntime<ThreadMessage>({
|
|
677
|
+
messages: EMPTY_MESSAGES,
|
|
678
|
+
onNew: async () => undefined,
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
useEffect(() => {
|
|
682
|
+
runtime.thread.composer.setText(props.value);
|
|
683
|
+
}, [props.value, runtime]);
|
|
684
|
+
|
|
685
|
+
return (
|
|
686
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
687
|
+
<ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
688
|
+
<CloudOsChatInputContent {...props} runtime={runtime} />
|
|
689
|
+
</ComposerPrimitive.Unstable_TriggerPopoverRoot>
|
|
690
|
+
</AssistantRuntimeProvider>
|
|
691
|
+
);
|
|
692
|
+
}
|
package/cloud-os/index.ts
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
export { CloudOsRoot } from "./layout/cloud-os-root";
|
|
14
|
+
export { CapabilityChip } from "./capability-chip";
|
|
15
|
+
export type { CapabilityChipProps } from "./capability-chip";
|
|
14
16
|
export { CloudOsWorkspaceSplit } from "./layout/cloud-os-workspace-split";
|
|
15
17
|
export type { CloudOsWorkspaceSplitProps } from "./layout/cloud-os-workspace-split";
|
|
16
18
|
|
|
@@ -23,6 +25,7 @@ export type {
|
|
|
23
25
|
export { CloudOsChatInput } from "./composer/cloud-os-chat-input";
|
|
24
26
|
export type {
|
|
25
27
|
CloudOsAttachmentView,
|
|
28
|
+
CloudOsCapabilityView,
|
|
26
29
|
CloudOsChatInputProps,
|
|
27
30
|
CloudOsComposerCommand,
|
|
28
31
|
CloudOsModelOption,
|
|
@@ -70,6 +73,7 @@ export {
|
|
|
70
73
|
formatClockTime,
|
|
71
74
|
formatFullTimestamp,
|
|
72
75
|
messageText,
|
|
76
|
+
requestedCapabilitiesOf,
|
|
73
77
|
rhythmTopClass,
|
|
74
78
|
} from "./chat/transcript-model";
|
|
75
79
|
export type {
|
|
@@ -77,6 +81,7 @@ export type {
|
|
|
77
81
|
CloudOsActivity,
|
|
78
82
|
CloudOsAttachment,
|
|
79
83
|
CloudOsEntry,
|
|
84
|
+
CloudOsRequestedCapability,
|
|
80
85
|
ParallelTool,
|
|
81
86
|
PlanStep,
|
|
82
87
|
SubAgentView,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
/**
|
|
11
11
|
* 左聊天 / 右工作区的双栏骨架。从 cloudflare-os-main `GadgetEditor.tsx` 的 BODY
|
|
12
12
|
* 段拷来:同一条 1px 的 kumo-line 分隔即拖拽把手、同一套 pointer capture 拖拽
|
|
13
|
-
* (拖过 iframe 也不丢)、同一个 200ms
|
|
13
|
+
* (拖过 iframe 也不丢)、同一个 200ms 的收起/展开过渡。
|
|
14
14
|
*
|
|
15
15
|
* 适配点:原文件把左右两侧的内容写死成 ChatInterface / GadgetUI,这里换成
|
|
16
16
|
* children 插槽;chatWidth 的 localStorage key 换成 cloud-os 自己的。
|
|
@@ -59,8 +59,6 @@ export interface CloudOsWorkspaceSplitProps {
|
|
|
59
59
|
workspace: ReactNode;
|
|
60
60
|
/** 关掉右栏时聊天占满整宽。 */
|
|
61
61
|
workspaceOpen: boolean;
|
|
62
|
-
/** 顶部那条推进条:回合进行中显示。 */
|
|
63
|
-
isAgentActive?: boolean;
|
|
64
62
|
/** 右栏收起时依然保留的窄边栏宽度(原文件的 Outputs rail)。 */
|
|
65
63
|
railWidth?: number;
|
|
66
64
|
rail?: ReactNode;
|
|
@@ -70,7 +68,6 @@ export function CloudOsWorkspaceSplit({
|
|
|
70
68
|
chat,
|
|
71
69
|
workspace,
|
|
72
70
|
workspaceOpen,
|
|
73
|
-
isAgentActive = false,
|
|
74
71
|
railWidth = 0,
|
|
75
72
|
rail,
|
|
76
73
|
}: CloudOsWorkspaceSplitProps) {
|
|
@@ -188,17 +185,6 @@ export function CloudOsWorkspaceSplit({
|
|
|
188
185
|
ref={containerRef}
|
|
189
186
|
className="relative flex h-full min-h-0 flex-1 overflow-hidden bg-kumo-base"
|
|
190
187
|
>
|
|
191
|
-
{isAgentActive && (
|
|
192
|
-
<div
|
|
193
|
-
className="absolute left-0 z-10 h-0"
|
|
194
|
-
style={{ top: 0, right: railWidth }}
|
|
195
|
-
>
|
|
196
|
-
<div className="absolute left-0 right-0 h-0.5 overflow-hidden bg-kumo-fill">
|
|
197
|
-
<div className="cos-progress-sweep absolute inset-y-0 w-1/3 bg-kumo-brand" />
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
)}
|
|
201
|
-
|
|
202
188
|
{/* ── 左:聊天 ──────────────────────────────────────────────────────── */}
|
|
203
189
|
<div
|
|
204
190
|
className={`flex h-full min-h-0 flex-shrink-0 flex-col ${transitionClass} ${
|
|
@@ -356,23 +356,6 @@
|
|
|
356
356
|
background: transparent;
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
-
/* ── 顶部推进条(isAgentActive)──────────────────────────────────────────── */
|
|
360
|
-
@keyframes cos-thinking {
|
|
361
|
-
0% {
|
|
362
|
-
left: -33%;
|
|
363
|
-
}
|
|
364
|
-
50% {
|
|
365
|
-
left: 100%;
|
|
366
|
-
}
|
|
367
|
-
100% {
|
|
368
|
-
left: -33%;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
.cos-progress-sweep {
|
|
373
|
-
animation: cos-thinking 1.5s ease-in-out infinite;
|
|
374
|
-
}
|
|
375
|
-
|
|
376
359
|
/* ============================================================================
|
|
377
360
|
* 以下整段来自 ChatInterface.module.css —— CSS Module 类名改成 cos- 全局前缀
|
|
378
361
|
* ========================================================================== */
|
|
@@ -550,9 +533,6 @@
|
|
|
550
533
|
background: none;
|
|
551
534
|
-webkit-text-fill-color: currentColor;
|
|
552
535
|
}
|
|
553
|
-
.cos-progress-sweep {
|
|
554
|
-
animation: none;
|
|
555
|
-
}
|
|
556
536
|
}
|
|
557
537
|
|
|
558
538
|
/* ── 观察类 markdown(行内、紧凑)────────────────────────────────────────── */
|
|
@@ -283,7 +283,6 @@ export function CloudOsChatShowcase() {
|
|
|
283
283
|
|
|
284
284
|
<CloudOsWorkspaceSplit
|
|
285
285
|
workspaceOpen={workspaceOpen}
|
|
286
|
-
isAgentActive={status === "submitted" || status === "streaming"}
|
|
287
286
|
chat={
|
|
288
287
|
<div className="flex h-full min-h-0 flex-col bg-kumo-base">
|
|
289
288
|
<aside
|
package/package.json
CHANGED
|
@@ -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
|
)}
|