@springbrand/message-panel 0.1.3-alpha.2 → 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/activity-indicator.tsx +29 -0
- package/cloud-os/chat/cloud-os-chat-messages.tsx +55 -17
- package/cloud-os/chat/rich-blocks.tsx +115 -83
- package/cloud-os/chat/tool-presentation.ts +42 -0
- package/cloud-os/chat/transcript-model.ts +124 -17
- package/cloud-os/composer/cloud-os-chat-input.tsx +184 -24
- package/cloud-os/index.ts +9 -0
- package/cloud-os/layout/cloud-os-workspace-split.tsx +50 -24
- package/cloud-os/styles/cloud-os.css +14 -20
- package/demo/cloud-os-chat-showcase.tsx +6 -6
- package/package.json +1 -1
- package/src/camel/camel-chat-messages.tsx +5 -0
- package/src/camel/camel-prompt-input.tsx +2 -1
- package/src/chat-summary-panel.tsx +1 -1
- package/src/composer/chat-composer.tsx +2 -1
- package/src/composer/composer-trigger-popover.tsx +1 -1
- package/src/composer/composer.tsx +2 -1
- package/src/composer/index.ts +1 -0
- package/src/composer/key-rules.ts +12 -0
- package/src/styles/index.css +4 -1
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { UIMessage } from "ai";
|
|
2
|
+
import type { OrbState } from "thinking-orbs";
|
|
2
3
|
import {
|
|
4
|
+
askUserOutcome,
|
|
3
5
|
buildToolCallGroups,
|
|
4
6
|
toCloudOsToolCall,
|
|
5
7
|
type CloudOsToolCall,
|
|
@@ -27,6 +29,13 @@ export interface CloudOsMessageMetadata extends UnknownRecord {
|
|
|
27
29
|
turnDurationMs?: number;
|
|
28
30
|
turnStartedAt?: number;
|
|
29
31
|
turnStatus?: string;
|
|
32
|
+
requestedCapabilities?: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CloudOsRequestedCapability {
|
|
36
|
+
kind: "skill" | "plan";
|
|
37
|
+
name: string;
|
|
38
|
+
label: string;
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
export interface CloudOsAttachment {
|
|
@@ -45,9 +54,12 @@ export type AssistantBlock =
|
|
|
45
54
|
| { kind: "text"; key: string; text: string }
|
|
46
55
|
| { kind: "toolGroup"; key: string; group: ToolCallGroup }
|
|
47
56
|
| { kind: "plan"; key: string; steps: PlanStep[]; running: boolean }
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
57
|
+
// 答案就在 call.output 里(`ask_user` 是 client-settled tool,用户点选后
|
|
58
|
+
// 经 respondToolInteraction 回写成这次调用的结果)。曾经靠扫下一条用户消息
|
|
59
|
+
// 推断,那是有损且会误认无关消息的,已彻底删除。
|
|
60
|
+
// `pending` 是这张卡还在等人 —— 卡片据此决定能不能点,活动指示器据此决定
|
|
61
|
+
// 该不该出现,同一条判据(askUserOutcome)算一次。
|
|
62
|
+
| { kind: "askUser"; key: string; call: CloudOsToolCall; pending: boolean }
|
|
51
63
|
| { kind: "suggestions"; key: string; items: string[] }
|
|
52
64
|
| { kind: "schedule"; key: string; call: CloudOsToolCall }
|
|
53
65
|
| { kind: "approval"; key: string; call: CloudOsToolCall; approvalId: string }
|
|
@@ -75,6 +87,7 @@ export type CloudOsEntry =
|
|
|
75
87
|
messageId: string;
|
|
76
88
|
text: string;
|
|
77
89
|
attachments: CloudOsAttachment[];
|
|
90
|
+
capabilities: CloudOsRequestedCapability[];
|
|
78
91
|
authorName?: string;
|
|
79
92
|
timestamp?: number;
|
|
80
93
|
}
|
|
@@ -121,6 +134,31 @@ export function cloudOsMetadata(message: UIMessage): CloudOsMessageMetadata {
|
|
|
121
134
|
return recordOf(message.metadata) as CloudOsMessageMetadata;
|
|
122
135
|
}
|
|
123
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
|
+
|
|
124
162
|
export function messageText(message: UIMessage): string {
|
|
125
163
|
return message.parts
|
|
126
164
|
.filter(
|
|
@@ -248,7 +286,6 @@ function assistantBlocks(
|
|
|
248
286
|
message: UIMessage,
|
|
249
287
|
isActive: boolean,
|
|
250
288
|
showThinkingTraces: boolean,
|
|
251
|
-
replyText: string | undefined,
|
|
252
289
|
approvalIdOf: (part: unknown) => string | undefined,
|
|
253
290
|
): AssistantBlock[] {
|
|
254
291
|
const blocks: AssistantBlock[] = [];
|
|
@@ -380,10 +417,7 @@ function assistantBlocks(
|
|
|
380
417
|
kind: "askUser",
|
|
381
418
|
key,
|
|
382
419
|
call,
|
|
383
|
-
|
|
384
|
-
replyText?.trim() ||
|
|
385
|
-
(typeof call.output === "string" ? call.output.trim() : "") ||
|
|
386
|
-
undefined,
|
|
420
|
+
pending: askUserOutcome(call).kind === "pending",
|
|
387
421
|
});
|
|
388
422
|
return;
|
|
389
423
|
}
|
|
@@ -432,13 +466,6 @@ function terminalOf(
|
|
|
432
466
|
return undefined;
|
|
433
467
|
}
|
|
434
468
|
|
|
435
|
-
function isDirectUserMessage(message: UIMessage): boolean {
|
|
436
|
-
return (
|
|
437
|
-
message.role === "user" &&
|
|
438
|
-
!message.parts.some((part) => dataKindOf(part) !== null)
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
|
|
442
469
|
export interface BuildEntriesOptions {
|
|
443
470
|
messages: readonly UIMessage[];
|
|
444
471
|
/** streaming/submitted 时最后一条 assistant 消息才算「活着」。 */
|
|
@@ -494,6 +521,7 @@ export function buildCloudOsEntries({
|
|
|
494
521
|
messageId: message.id,
|
|
495
522
|
text,
|
|
496
523
|
attachments,
|
|
524
|
+
capabilities: requestedCapabilitiesOf(metadata),
|
|
497
525
|
authorName: metadata.authorDisplayName,
|
|
498
526
|
timestamp:
|
|
499
527
|
nonNegativeNumber(metadata.createdAt) ??
|
|
@@ -502,12 +530,10 @@ export function buildCloudOsEntries({
|
|
|
502
530
|
return;
|
|
503
531
|
}
|
|
504
532
|
|
|
505
|
-
const reply = messages.slice(index + 1).find(isDirectUserMessage);
|
|
506
533
|
const blocks = assistantBlocks(
|
|
507
534
|
message,
|
|
508
535
|
isActive && index === lastAssistantIndex,
|
|
509
536
|
showThinkingTraces,
|
|
510
|
-
reply ? messageText(reply).trim() || undefined : undefined,
|
|
511
537
|
approvalIdOf,
|
|
512
538
|
);
|
|
513
539
|
const terminal = terminalOf(message);
|
|
@@ -560,6 +586,87 @@ function dropSupersededPlans(entries: CloudOsEntry[]): CloudOsEntry[] {
|
|
|
560
586
|
);
|
|
561
587
|
}
|
|
562
588
|
|
|
589
|
+
// ── 活动指示(本回合此刻在干什么)────────────────────────────────────────────
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* 底部活动指示器要显示的动画与文案。
|
|
593
|
+
*/
|
|
594
|
+
export interface CloudOsActivity {
|
|
595
|
+
state: OrbState;
|
|
596
|
+
label: string;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* 尾巴是否**自己会说话** —— 有它在动,就不该再挂一个活动指示器。
|
|
601
|
+
*
|
|
602
|
+
* 正在流的正文/思考、还在跑的工具组或计划,以及一张**还等着你作答**的
|
|
603
|
+
* `ask_user` 卡都算:前三者自身就有动效,最后一个等的是人不是模型,这时候
|
|
604
|
+
* 显示「Thinking」是在撒谎。
|
|
605
|
+
*/
|
|
606
|
+
function isLiveBlock(block: AssistantBlock): boolean {
|
|
607
|
+
switch (block.kind) {
|
|
608
|
+
case "text":
|
|
609
|
+
case "reasoning":
|
|
610
|
+
return true;
|
|
611
|
+
case "toolGroup":
|
|
612
|
+
return block.group.hasRunning;
|
|
613
|
+
case "plan":
|
|
614
|
+
return block.running;
|
|
615
|
+
case "askUser":
|
|
616
|
+
return block.pending;
|
|
617
|
+
// approval 块只在 awaitingApproval 时才建出来,所以它在场就是在等人。
|
|
618
|
+
case "approval":
|
|
619
|
+
return true;
|
|
620
|
+
case "parallel":
|
|
621
|
+
return block.tools.some((tool) => tool.status === "running");
|
|
622
|
+
case "subagents":
|
|
623
|
+
return block.agents.some((agent) => agent.status === "running");
|
|
624
|
+
default:
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 已结算的尾巴 → 此刻真正在发生的事。措辞刻意只说得出口的那部分:
|
|
630
|
+
// 工具跑完了,模型在读它的输出;卡答完了,模型在接你的答案。
|
|
631
|
+
function activityForTail(block: AssistantBlock | undefined): CloudOsActivity {
|
|
632
|
+
if (!block) return { state: "solving", label: "Thinking…" };
|
|
633
|
+
if (block.kind === "askUser") {
|
|
634
|
+
return { state: "solving", label: "Picking up your answer…" };
|
|
635
|
+
}
|
|
636
|
+
if (block.kind === "toolGroup") {
|
|
637
|
+
const kind = block.group.calls.at(-1)?.kind;
|
|
638
|
+
return kind !== undefined &&
|
|
639
|
+
["web-search", "web-fetch", "grep", "glob", "list", "read"].includes(
|
|
640
|
+
kind,
|
|
641
|
+
)
|
|
642
|
+
? { state: "searching", label: "Reading results…" }
|
|
643
|
+
: { state: "working", label: "Working…" };
|
|
644
|
+
}
|
|
645
|
+
return { state: "working", label: "Working…" };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* 本回合此刻该显示什么活动;`null` = 尾巴自己会说话,别再叠一个。
|
|
650
|
+
*
|
|
651
|
+
* @remarks
|
|
652
|
+
* 判据是「**尾巴有没有活口**」,不是「本回合有没有出过内容」。后者曾经是这里的
|
|
653
|
+
* 写法,于是答完 `ask_user`(卡片变成 Submitted,读起来就是"结束了")到模型下一
|
|
654
|
+
* 个 chunk 之间整屏没有任何在跑的迹象 —— 而那段恰好是最长的一次 TTFT,看起来
|
|
655
|
+
* 就是卡死。凡是尾巴为**已结算卡片**的断档都是同一个洞,这里一并补上。
|
|
656
|
+
*/
|
|
657
|
+
export function deriveTurnActivity(
|
|
658
|
+
entries: readonly CloudOsEntry[],
|
|
659
|
+
options: { isActive: boolean; hasPendingSteer?: boolean },
|
|
660
|
+
): CloudOsActivity | null {
|
|
661
|
+
if (!options.isActive) return null;
|
|
662
|
+
const tail = entries.at(-1) ?? null;
|
|
663
|
+
const block = tail?.type === "assistant" ? tail.blocks.at(-1) : undefined;
|
|
664
|
+
if (block && isLiveBlock(block)) return null;
|
|
665
|
+
return options.hasPendingSteer
|
|
666
|
+
? { state: "solving", label: "Waiting for the current step to finish…" }
|
|
667
|
+
: activityForTail(block);
|
|
668
|
+
}
|
|
669
|
+
|
|
563
670
|
// ── 行间距节奏(照搬原 rhythmTopClass)──────────────────────────────────────
|
|
564
671
|
|
|
565
672
|
function isUserEntry(entry: CloudOsEntry): boolean {
|
|
@@ -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,12 @@ 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";
|
|
35
|
+
|
|
36
|
+
const isComposingKeyEvent = (event: {
|
|
37
|
+
isComposing?: boolean;
|
|
38
|
+
keyCode?: number;
|
|
39
|
+
}) => event.isComposing === true || event.keyCode === 229;
|
|
22
40
|
|
|
23
41
|
/**
|
|
24
42
|
* 从 cloudflare-os-main `ChatInterface.tsx` 的 `ChatInput` 拷来。
|
|
@@ -27,9 +45,10 @@ import { WorkshopIconButton } from "../primitives/workshop-controls";
|
|
|
27
45
|
* 附件缩略图行、底部左「+ / 添加资源」右「模型 / 发送-停止」的布局、
|
|
28
46
|
* draftUpdateBanner 插槽、blockedReason 占位文案、Enter 发送 / Shift+Enter 换行。
|
|
29
47
|
*
|
|
30
|
-
* 删掉:capsule(URL → 资源胶囊)、
|
|
31
|
-
*
|
|
32
|
-
*
|
|
48
|
+
* 删掉:capsule(URL → 资源胶囊)、ComposerMirror、GatekeeperModal —— 这三样都直挂
|
|
49
|
+
* gadgets 的 capnweb RPC,在 UIMessage 协议下没有对应物。「添加资源」按钮保留成一个
|
|
50
|
+
* 可选回调,宿主不给就不渲染。Command picker 使用 package 内的
|
|
51
|
+
* assistant-ui ComposerTriggerPopover。
|
|
33
52
|
*/
|
|
34
53
|
|
|
35
54
|
export interface CloudOsModelOption {
|
|
@@ -38,10 +57,15 @@ export interface CloudOsModelOption {
|
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
export interface CloudOsComposerCommand {
|
|
60
|
+
prefix?: "/" | "$";
|
|
41
61
|
name: string;
|
|
42
62
|
description: string;
|
|
43
|
-
|
|
44
|
-
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface CloudOsCapabilityView {
|
|
66
|
+
kind: "skill" | "plan";
|
|
67
|
+
name: string;
|
|
68
|
+
label: string;
|
|
45
69
|
}
|
|
46
70
|
|
|
47
71
|
export interface CloudOsAttachmentView {
|
|
@@ -77,11 +101,11 @@ export interface CloudOsChatInputProps {
|
|
|
77
101
|
* 传了就以它为准来决定显示发送键还是停止键。
|
|
78
102
|
*/
|
|
79
103
|
turnActive?: boolean;
|
|
80
|
-
/**
|
|
81
|
-
* 可用的斜杠命令。cloudflare-os 原版是一个直连 RPC 的行内 picker;这里退成
|
|
82
|
-
* 「+」菜单里的一组条目 —— 能力保住,但**没有**边打字边过滤的行内补全。
|
|
83
|
-
*/
|
|
104
|
+
/** 可用的斜杠命令,也会显示在「+」菜单中。 */
|
|
84
105
|
commands?: readonly CloudOsComposerCommand[];
|
|
106
|
+
capabilities?: readonly CloudOsCapabilityView[];
|
|
107
|
+
onCommandSelect?: (command: CloudOsComposerCommand) => void;
|
|
108
|
+
onCapabilityRemove?: (kind: CloudOsCapabilityView["kind"], name: string) => void;
|
|
85
109
|
/** 有值时渲染「添加资源」按钮。 */
|
|
86
110
|
onAttachResource?: () => void;
|
|
87
111
|
attachLabel?: string;
|
|
@@ -101,6 +125,10 @@ export interface CloudOsChatInputProps {
|
|
|
101
125
|
|
|
102
126
|
const MAX_PENDING_ATTACHMENTS = 5;
|
|
103
127
|
|
|
128
|
+
function commandLabel(command: CloudOsComposerCommand): string {
|
|
129
|
+
return `${command.prefix ?? "/"}${command.name.replaceAll("-", " ")}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
104
132
|
function autoResizeTextarea(
|
|
105
133
|
textarea: HTMLTextAreaElement,
|
|
106
134
|
minRows: number,
|
|
@@ -118,7 +146,7 @@ function autoResizeTextarea(
|
|
|
118
146
|
textarea.style.overflow = textarea.scrollHeight > maxH ? "auto" : "hidden";
|
|
119
147
|
}
|
|
120
148
|
|
|
121
|
-
|
|
149
|
+
function CloudOsChatInputContent({
|
|
122
150
|
value,
|
|
123
151
|
onChange,
|
|
124
152
|
onSubmit,
|
|
@@ -136,6 +164,9 @@ export function CloudOsChatInput({
|
|
|
136
164
|
submissionBlocked = false,
|
|
137
165
|
turnActive,
|
|
138
166
|
commands = [],
|
|
167
|
+
capabilities = [],
|
|
168
|
+
onCommandSelect,
|
|
169
|
+
onCapabilityRemove,
|
|
139
170
|
onAttachResource,
|
|
140
171
|
attachLabel,
|
|
141
172
|
showThinkingTraces,
|
|
@@ -147,7 +178,8 @@ export function CloudOsChatInput({
|
|
|
147
178
|
maxRows = 6,
|
|
148
179
|
autoFocus = false,
|
|
149
180
|
footnote,
|
|
150
|
-
|
|
181
|
+
runtime,
|
|
182
|
+
}: CloudOsChatInputProps & { runtime: AssistantRuntime }) {
|
|
151
183
|
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
|
|
152
184
|
const attachmentInputRef = useRef<HTMLInputElement>(null);
|
|
153
185
|
const dragDepthRef = useRef(0);
|
|
@@ -184,6 +216,73 @@ export function CloudOsChatInput({
|
|
|
184
216
|
if (element) autoResizeTextarea(element, minRows, maxRows);
|
|
185
217
|
}, [maxRows, minRows, value]);
|
|
186
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
|
+
|
|
187
286
|
const addFiles = useCallback(
|
|
188
287
|
(files: readonly File[]) => {
|
|
189
288
|
if (files.length === 0) return;
|
|
@@ -219,6 +318,20 @@ export function CloudOsChatInput({
|
|
|
219
318
|
return (
|
|
220
319
|
// isolation: isolate 把输入框内部的 z-index 关起来,免得盖到 portal 出去的模型下拉。
|
|
221
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
|
+
/>
|
|
222
335
|
<input
|
|
223
336
|
ref={attachmentInputRef}
|
|
224
337
|
type="file"
|
|
@@ -271,7 +384,22 @@ export function CloudOsChatInput({
|
|
|
271
384
|
|
|
272
385
|
{banner}
|
|
273
386
|
|
|
274
|
-
<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
|
+
)}
|
|
275
403
|
<textarea
|
|
276
404
|
ref={(element) => {
|
|
277
405
|
textareaRef.current = element;
|
|
@@ -289,9 +417,17 @@ export function CloudOsChatInput({
|
|
|
289
417
|
: (placeholder ?? "Ask a follow-up…")
|
|
290
418
|
}
|
|
291
419
|
onChange={(event) => {
|
|
292
|
-
|
|
420
|
+
const next = event.target.value;
|
|
421
|
+
runtime.thread.composer.setText(next);
|
|
422
|
+
onChange(next);
|
|
423
|
+
setTriggerCursor(event.target.selectionStart ?? next.length);
|
|
293
424
|
autoResizeTextarea(event.target, minRows, maxRows);
|
|
294
425
|
}}
|
|
426
|
+
onSelect={(event) =>
|
|
427
|
+
setTriggerCursor(
|
|
428
|
+
event.currentTarget.selectionStart ?? value.length,
|
|
429
|
+
)
|
|
430
|
+
}
|
|
295
431
|
onPaste={(event) => {
|
|
296
432
|
const files = Array.from(event.clipboardData.items)
|
|
297
433
|
.filter((item) => item.kind === "file")
|
|
@@ -303,13 +439,21 @@ export function CloudOsChatInput({
|
|
|
303
439
|
}
|
|
304
440
|
}}
|
|
305
441
|
onKeyDown={(event) => {
|
|
306
|
-
|
|
307
|
-
|
|
442
|
+
for (const trigger of triggers.values()) {
|
|
443
|
+
if (trigger.resource.handleKeyDown(event)) return;
|
|
444
|
+
}
|
|
445
|
+
// Enter 发送(按住 Shift 换行); IME 组合中不发送
|
|
446
|
+
if (
|
|
447
|
+
event.key === "Enter" &&
|
|
448
|
+
!event.shiftKey &&
|
|
449
|
+
!isComposingKeyEvent(event.nativeEvent)
|
|
450
|
+
) {
|
|
308
451
|
event.preventDefault();
|
|
309
452
|
if (canSend) onSubmit();
|
|
310
453
|
}
|
|
311
454
|
}}
|
|
312
|
-
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}
|
|
313
457
|
/>
|
|
314
458
|
</div>
|
|
315
459
|
|
|
@@ -395,20 +539,15 @@ export function CloudOsChatInput({
|
|
|
395
539
|
<>
|
|
396
540
|
{commands.map((command) => (
|
|
397
541
|
<DropdownMenu.Item
|
|
398
|
-
key={command.name}
|
|
399
|
-
onClick={() =>
|
|
400
|
-
onChange(command.prompt);
|
|
401
|
-
requestAnimationFrame(() =>
|
|
402
|
-
textareaRef.current?.focus(),
|
|
403
|
-
);
|
|
404
|
-
}}
|
|
542
|
+
key={`${command.prefix ?? "/"}${command.name}`}
|
|
543
|
+
onClick={() => selectCommand(command, false)}
|
|
405
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"
|
|
406
545
|
>
|
|
407
546
|
<span className="mr-2 inline-flex h-4 w-4 items-center justify-center text-kumo-inactive">
|
|
408
547
|
<Terminal size={14} />
|
|
409
548
|
</span>
|
|
410
549
|
<span className="min-w-0 flex-1 truncate">
|
|
411
|
-
|
|
550
|
+
{commandLabel(command)}
|
|
412
551
|
</span>
|
|
413
552
|
</DropdownMenu.Item>
|
|
414
553
|
))}
|
|
@@ -530,3 +669,24 @@ export function CloudOsChatInput({
|
|
|
530
669
|
</div>
|
|
531
670
|
);
|
|
532
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,
|
|
@@ -61,18 +64,24 @@ export {
|
|
|
61
64
|
} from "./chat/rich-blocks";
|
|
62
65
|
export type { ApprovalDecision } from "./chat/rich-blocks";
|
|
63
66
|
|
|
67
|
+
export { CloudOsActivityIndicator } from "./chat/activity-indicator";
|
|
68
|
+
|
|
64
69
|
export {
|
|
65
70
|
buildCloudOsEntries,
|
|
66
71
|
cloudOsMetadata,
|
|
72
|
+
deriveTurnActivity,
|
|
67
73
|
formatClockTime,
|
|
68
74
|
formatFullTimestamp,
|
|
69
75
|
messageText,
|
|
76
|
+
requestedCapabilitiesOf,
|
|
70
77
|
rhythmTopClass,
|
|
71
78
|
} from "./chat/transcript-model";
|
|
72
79
|
export type {
|
|
73
80
|
AssistantBlock,
|
|
81
|
+
CloudOsActivity,
|
|
74
82
|
CloudOsAttachment,
|
|
75
83
|
CloudOsEntry,
|
|
84
|
+
CloudOsRequestedCapability,
|
|
76
85
|
ParallelTool,
|
|
77
86
|
PlanStep,
|
|
78
87
|
SubAgentView,
|