@springbrand/message-panel 0.1.3-alpha.19 → 0.1.3-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { memo, useMemo, type ReactNode } from "react";
|
|
2
|
-
import
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
defaultRehypePlugins,
|
|
4
|
+
Streamdown,
|
|
5
|
+
type Components,
|
|
6
|
+
type StreamdownProps,
|
|
7
|
+
} from "streamdown";
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* UIMessage 协议里没有对应概念,留着只会是死代码。
|
|
9
|
-
* 保留的部分:skipHtml、remark-gfm、表格外包一层横向滚动、外链白名单 + target=_blank。
|
|
10
|
-
* 加上的部分:resolveUrl —— 本仓的消息里会出现 `sandbox:/workspace/...` 这种
|
|
11
|
-
* 只有宿主知道怎么翻译成可访问 URL 的地址,原来由 camel 的同名钩子负责。
|
|
10
|
+
* Streamdown 依然使用原生标签,避免它的默认 Tailwind 组件样式覆盖 cloud-os CSS。
|
|
11
|
+
* resolveUrl 负责把 `sandbox:/workspace/...` 翻译成可访问 URL。
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
export type CloudOsUrlResolver = (url: string) => string | null | undefined;
|
|
@@ -25,35 +25,71 @@ export function safeExternalUrl(href: string | undefined): string | undefined {
|
|
|
25
25
|
}
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
51
|
-
}
|
|
28
|
+
const STREAMDOWN_REHYPE_PLUGINS = Object.values(
|
|
29
|
+
defaultRehypePlugins,
|
|
30
|
+
) as NonNullable<StreamdownProps["rehypePlugins"]>;
|
|
31
|
+
|
|
32
|
+
type UrlNode = {
|
|
33
|
+
type?: string;
|
|
34
|
+
properties?: Record<string, unknown>;
|
|
35
|
+
children?: UrlNode[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function resolveUrlsPlugin(resolveUrl: CloudOsUrlResolver) {
|
|
39
|
+
return () => (tree: UrlNode) => {
|
|
40
|
+
const visit = (node: UrlNode): void => {
|
|
41
|
+
if (node.type === "element" && node.properties) {
|
|
42
|
+
for (const attribute of ["href", "src"]) {
|
|
43
|
+
const url = node.properties[attribute];
|
|
44
|
+
if (typeof url !== "string") continue;
|
|
45
|
+
const resolved = resolveUrl(url);
|
|
46
|
+
if (resolved === null) delete node.properties[attribute];
|
|
47
|
+
else if (resolved !== undefined) node.properties[attribute] = resolved;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
node.children?.forEach(visit);
|
|
51
|
+
};
|
|
52
|
+
visit(tree);
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
const
|
|
56
|
-
|
|
56
|
+
const DEFAULT_COMPONENTS: Components = {
|
|
57
|
+
blockquote: "blockquote",
|
|
58
|
+
code: "code",
|
|
59
|
+
h1: "h1",
|
|
60
|
+
h2: "h2",
|
|
61
|
+
h3: "h3",
|
|
62
|
+
h4: "h4",
|
|
63
|
+
h5: "h5",
|
|
64
|
+
h6: "h6",
|
|
65
|
+
hr: "hr",
|
|
66
|
+
li: "li",
|
|
67
|
+
ol: "ol",
|
|
68
|
+
p: "p",
|
|
69
|
+
pre: "pre",
|
|
70
|
+
section: "section",
|
|
71
|
+
strong: "strong",
|
|
72
|
+
sub: "sub",
|
|
73
|
+
sup: "sup",
|
|
74
|
+
table: "table",
|
|
75
|
+
tbody: "tbody",
|
|
76
|
+
td: "td",
|
|
77
|
+
th: "th",
|
|
78
|
+
thead: "thead",
|
|
79
|
+
tr: "tr",
|
|
80
|
+
ul: "ul",
|
|
81
|
+
img: ({ node: _node, src, ...props }) =>
|
|
82
|
+
src ? <img {...props} src={src} /> : null,
|
|
83
|
+
a: ({ node: _node, href, children, ...props }) => {
|
|
84
|
+
const safeHref = safeExternalUrl(href);
|
|
85
|
+
if (!safeHref) return <>{children}</>;
|
|
86
|
+
return (
|
|
87
|
+
<a {...props} href={safeHref} target="_blank" rel="noopener noreferrer">
|
|
88
|
+
{children}
|
|
89
|
+
</a>
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
57
93
|
|
|
58
94
|
export const MarkdownMessage = memo(function MarkdownMessage({
|
|
59
95
|
message,
|
|
@@ -62,17 +98,23 @@ export const MarkdownMessage = memo(function MarkdownMessage({
|
|
|
62
98
|
message: string;
|
|
63
99
|
resolveUrl?: CloudOsUrlResolver;
|
|
64
100
|
}): ReactNode {
|
|
65
|
-
const
|
|
66
|
-
() =>
|
|
101
|
+
const rehypePlugins = useMemo(
|
|
102
|
+
() =>
|
|
103
|
+
resolveUrl
|
|
104
|
+
? [resolveUrlsPlugin(resolveUrl), ...STREAMDOWN_REHYPE_PLUGINS]
|
|
105
|
+
: STREAMDOWN_REHYPE_PLUGINS,
|
|
67
106
|
[resolveUrl],
|
|
68
107
|
);
|
|
69
108
|
return (
|
|
70
|
-
<
|
|
109
|
+
<Streamdown
|
|
110
|
+
className="space-y-0"
|
|
111
|
+
controls={false}
|
|
112
|
+
mode="static"
|
|
113
|
+
rehypePlugins={rehypePlugins}
|
|
71
114
|
skipHtml
|
|
72
|
-
|
|
73
|
-
components={components}
|
|
115
|
+
components={DEFAULT_COMPONENTS}
|
|
74
116
|
>
|
|
75
117
|
{message}
|
|
76
|
-
</
|
|
118
|
+
</Streamdown>
|
|
77
119
|
);
|
|
78
120
|
});
|
|
@@ -298,7 +298,8 @@ export function getToolCallSummary(call: CloudOsToolCall): {
|
|
|
298
298
|
return { verb: running ? "Editing" : "Edited", target: filename || undefined };
|
|
299
299
|
case "bash": {
|
|
300
300
|
const command =
|
|
301
|
-
stringField(input, "description") ||
|
|
301
|
+
stringField(input, "description") ||
|
|
302
|
+
stringField(input, "script", "command", "cmd");
|
|
302
303
|
return {
|
|
303
304
|
verb: running ? "Running" : "Ran",
|
|
304
305
|
target: command ? truncate(command, 60) : undefined,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@springbrand/message-panel",
|
|
3
|
-
"version": "0.1.3-alpha.
|
|
3
|
+
"version": "0.1.3-alpha.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -33,8 +33,6 @@
|
|
|
33
33
|
"lucide-react": "^1.24.0",
|
|
34
34
|
"motion": "^13.1.0",
|
|
35
35
|
"radix-ui": "^1.6.2",
|
|
36
|
-
"react-markdown": "^10.1.0",
|
|
37
|
-
"remark-gfm": "^4.0.1",
|
|
38
36
|
"streamdown": "^2.5.0",
|
|
39
37
|
"thinking-orbs": "0.2.0",
|
|
40
38
|
"use-stick-to-bottom": "^1.1.6"
|
|
@@ -383,7 +383,7 @@ function summaryFor(
|
|
|
383
383
|
complete: "Edited",
|
|
384
384
|
});
|
|
385
385
|
case "bash": {
|
|
386
|
-
const command = stringField(input, "command", "cmd");
|
|
386
|
+
const command = stringField(input, "script", "command", "cmd");
|
|
387
387
|
const description = stringField(input, "description");
|
|
388
388
|
const label = description || truncate(command || "command", 30);
|
|
389
389
|
return running
|
|
@@ -818,7 +818,7 @@ function EditDetails({ presentation }: CamelToolDetailsProps) {
|
|
|
818
818
|
|
|
819
819
|
function BashDetails({ presentation }: CamelToolDetailsProps) {
|
|
820
820
|
const { input, output, outputText } = presentation;
|
|
821
|
-
const command = stringField(input, "command", "cmd");
|
|
821
|
+
const command = stringField(input, "script", "command", "cmd");
|
|
822
822
|
const outputRecord = recordOf(output);
|
|
823
823
|
const parsedFromText = (() => {
|
|
824
824
|
if (!outputText.trim().startsWith("{")) return {};
|