@protolabsai/ui 0.21.0 → 0.22.1
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/dist/plugin-kit.css +182 -0
- package/package.json +2 -1
- package/src/ToolCard.stories.tsx +90 -0
- package/src/styles/tool-card.css +180 -0
- package/src/styles.css +1 -0
- package/src/tool-card.tsx +184 -0
package/dist/plugin-kit.css
CHANGED
|
@@ -2869,6 +2869,188 @@ a.pl-changelog__version:hover {
|
|
|
2869
2869
|
gap: var(--pl-space-2);
|
|
2870
2870
|
}
|
|
2871
2871
|
|
|
2872
|
+
/* ── ui component: tool-card.css ───────────────────────────────────────────── */
|
|
2873
|
+
/* @protolabsai/ui — ToolCard (streamed tool-call disclosure). Ported from the
|
|
2874
|
+
protoAgent console's tool-card family onto --pl-* tokens. The frame only — the
|
|
2875
|
+
body is a host-filled slot (per-tool value rendering stays app-side). */
|
|
2876
|
+
|
|
2877
|
+
.pl-toolcard-list {
|
|
2878
|
+
display: flex;
|
|
2879
|
+
flex-direction: column;
|
|
2880
|
+
gap: var(--pl-space-2);
|
|
2881
|
+
min-width: 0;
|
|
2882
|
+
}
|
|
2883
|
+
.pl-toolcard-group {
|
|
2884
|
+
display: flex;
|
|
2885
|
+
flex-direction: column;
|
|
2886
|
+
gap: 6px;
|
|
2887
|
+
min-width: 0;
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
.pl-toolcard {
|
|
2891
|
+
min-width: 0;
|
|
2892
|
+
max-width: 100%;
|
|
2893
|
+
background: var(--pl-color-bg-raised);
|
|
2894
|
+
border: var(--pl-border-width) solid var(--pl-color-border);
|
|
2895
|
+
border-radius: var(--pl-radius);
|
|
2896
|
+
overflow: hidden;
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
.pl-toolcard__head-row {
|
|
2900
|
+
display: flex;
|
|
2901
|
+
align-items: center;
|
|
2902
|
+
}
|
|
2903
|
+
.pl-toolcard__head {
|
|
2904
|
+
flex: 1 1 auto;
|
|
2905
|
+
min-width: 0;
|
|
2906
|
+
display: flex;
|
|
2907
|
+
align-items: center;
|
|
2908
|
+
gap: 7px;
|
|
2909
|
+
padding: 6px 9px;
|
|
2910
|
+
background: none;
|
|
2911
|
+
border: none;
|
|
2912
|
+
color: var(--pl-color-fg-muted);
|
|
2913
|
+
font: inherit;
|
|
2914
|
+
font-size: 12px;
|
|
2915
|
+
text-align: left;
|
|
2916
|
+
cursor: pointer;
|
|
2917
|
+
}
|
|
2918
|
+
.pl-toolcard__actions {
|
|
2919
|
+
flex: none;
|
|
2920
|
+
display: inline-flex;
|
|
2921
|
+
align-items: center;
|
|
2922
|
+
gap: 2px;
|
|
2923
|
+
padding-right: 6px;
|
|
2924
|
+
color: var(--pl-color-fg-muted);
|
|
2925
|
+
}
|
|
2926
|
+
.pl-toolcard__head:disabled {
|
|
2927
|
+
cursor: default;
|
|
2928
|
+
}
|
|
2929
|
+
.pl-toolcard__head:hover:not(:disabled) .pl-toolcard__name {
|
|
2930
|
+
color: var(--pl-color-fg);
|
|
2931
|
+
}
|
|
2932
|
+
|
|
2933
|
+
.pl-toolcard__caret {
|
|
2934
|
+
flex: none;
|
|
2935
|
+
display: inline-flex;
|
|
2936
|
+
color: var(--pl-color-fg-subtle);
|
|
2937
|
+
transition: transform var(--pl-motion-fast) var(--pl-motion-ease);
|
|
2938
|
+
}
|
|
2939
|
+
.pl-toolcard__caret--open {
|
|
2940
|
+
transform: rotate(90deg);
|
|
2941
|
+
}
|
|
2942
|
+
.pl-toolcard__caret--hidden {
|
|
2943
|
+
visibility: hidden;
|
|
2944
|
+
}
|
|
2945
|
+
|
|
2946
|
+
.pl-toolcard__icon {
|
|
2947
|
+
flex: none;
|
|
2948
|
+
display: inline-flex;
|
|
2949
|
+
color: var(--pl-color-fg-muted);
|
|
2950
|
+
}
|
|
2951
|
+
.pl-toolcard__icon svg {
|
|
2952
|
+
width: 13px;
|
|
2953
|
+
height: 13px;
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
.pl-toolcard__name {
|
|
2957
|
+
flex: 1 1 auto;
|
|
2958
|
+
min-width: 0;
|
|
2959
|
+
overflow: hidden;
|
|
2960
|
+
text-overflow: ellipsis;
|
|
2961
|
+
white-space: nowrap;
|
|
2962
|
+
font-family: var(--pl-font-mono);
|
|
2963
|
+
color: var(--pl-color-fg);
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
.pl-toolcard__dur {
|
|
2967
|
+
flex: none;
|
|
2968
|
+
font-family: var(--pl-font-mono);
|
|
2969
|
+
font-size: 10px;
|
|
2970
|
+
color: var(--pl-color-fg-muted);
|
|
2971
|
+
font-variant-numeric: tabular-nums;
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
.pl-toolcard__status {
|
|
2975
|
+
flex: none;
|
|
2976
|
+
display: inline-flex;
|
|
2977
|
+
}
|
|
2978
|
+
.pl-toolcard__status--done {
|
|
2979
|
+
color: var(--pl-color-status-success);
|
|
2980
|
+
}
|
|
2981
|
+
.pl-toolcard__status--error {
|
|
2982
|
+
color: var(--pl-color-status-error);
|
|
2983
|
+
}
|
|
2984
|
+
.pl-toolcard__status--running {
|
|
2985
|
+
color: var(--pl-color-status-warning);
|
|
2986
|
+
}
|
|
2987
|
+
.pl-toolcard__spin {
|
|
2988
|
+
animation: pl-toolcard-spin 0.8s linear infinite;
|
|
2989
|
+
}
|
|
2990
|
+
@keyframes pl-toolcard-spin {
|
|
2991
|
+
to {
|
|
2992
|
+
transform: rotate(360deg);
|
|
2993
|
+
}
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
.pl-toolcard__body {
|
|
2997
|
+
display: flex;
|
|
2998
|
+
flex-direction: column;
|
|
2999
|
+
gap: 8px;
|
|
3000
|
+
padding: 8px 9px;
|
|
3001
|
+
border-top: var(--pl-border-width) solid var(--pl-color-border);
|
|
3002
|
+
}
|
|
3003
|
+
|
|
3004
|
+
/* subagent `task` nesting — indented rail of child cards */
|
|
3005
|
+
.pl-toolcard__children {
|
|
3006
|
+
display: flex;
|
|
3007
|
+
flex-direction: column;
|
|
3008
|
+
gap: 6px;
|
|
3009
|
+
min-width: 0;
|
|
3010
|
+
margin-left: 14px;
|
|
3011
|
+
padding-left: 10px;
|
|
3012
|
+
border-left: 2px solid var(--pl-color-border);
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
/* body section (input / result) */
|
|
3016
|
+
.pl-toolcard__section {
|
|
3017
|
+
display: flex;
|
|
3018
|
+
flex-direction: column;
|
|
3019
|
+
gap: 4px;
|
|
3020
|
+
min-width: 0;
|
|
3021
|
+
}
|
|
3022
|
+
.pl-toolcard__section-head {
|
|
3023
|
+
display: flex;
|
|
3024
|
+
align-items: center;
|
|
3025
|
+
justify-content: space-between;
|
|
3026
|
+
gap: 8px;
|
|
3027
|
+
}
|
|
3028
|
+
.pl-toolcard__label {
|
|
3029
|
+
font-family: var(--pl-font-mono);
|
|
3030
|
+
font-size: 10px;
|
|
3031
|
+
text-transform: uppercase;
|
|
3032
|
+
letter-spacing: 0.04em;
|
|
3033
|
+
color: var(--pl-color-fg-subtle);
|
|
3034
|
+
}
|
|
3035
|
+
.pl-toolcard__copy {
|
|
3036
|
+
display: inline-flex;
|
|
3037
|
+
align-items: center;
|
|
3038
|
+
justify-content: center;
|
|
3039
|
+
padding: 2px;
|
|
3040
|
+
background: none;
|
|
3041
|
+
border: none;
|
|
3042
|
+
border-radius: var(--pl-radius);
|
|
3043
|
+
color: var(--pl-color-fg-muted);
|
|
3044
|
+
cursor: pointer;
|
|
3045
|
+
transition:
|
|
3046
|
+
background var(--pl-motion-fast) var(--pl-motion-ease),
|
|
3047
|
+
color var(--pl-motion-fast) var(--pl-motion-ease);
|
|
3048
|
+
}
|
|
3049
|
+
.pl-toolcard__copy:hover {
|
|
3050
|
+
color: var(--pl-color-fg);
|
|
3051
|
+
background: var(--pl-color-bg-hover);
|
|
3052
|
+
}
|
|
3053
|
+
|
|
2872
3054
|
/* ── ui component: theming.css ─────────────────────────────────────────────── */
|
|
2873
3055
|
/* @protolabsai/ui — ThemePanel (live token editor). All --pl-* driven, so the
|
|
2874
3056
|
panel itself re-skins live as you edit. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protolabsai/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"registry": "https://registry.npmjs.org/"
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"./data": "./src/data.tsx",
|
|
22
22
|
"./menu": "./src/menu.tsx",
|
|
23
23
|
"./app-shell": "./src/app-shell.tsx",
|
|
24
|
+
"./tool-card": "./src/tool-card.tsx",
|
|
24
25
|
"./theming": "./src/theming.tsx",
|
|
25
26
|
"./styles.css": "./src/styles.css",
|
|
26
27
|
"./plugin-kit.css": "./dist/plugin-kit.css",
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { ToolCard, ToolCardList, ToolSection } from "./tool-card";
|
|
3
|
+
|
|
4
|
+
const meta: Meta = { title: "Components/ToolCard" };
|
|
5
|
+
export default meta;
|
|
6
|
+
type Story = StoryObj;
|
|
7
|
+
|
|
8
|
+
const Mono = ({ children }: { children: string }) => (
|
|
9
|
+
<pre style={{ margin: 0, fontFamily: "var(--pl-font-mono)", fontSize: 12, color: "var(--pl-color-fg-muted)", whiteSpace: "pre-wrap" }}>{children}</pre>
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const Globe = () => (
|
|
13
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
|
|
14
|
+
<circle cx="12" cy="12" r="9" /><path d="M3 12h18M12 3a14 14 0 0 1 0 18M12 3a14 14 0 0 0 0 18" />
|
|
15
|
+
</svg>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
/** The three states + a collapsed (no-detail) running call. Body is a host slot —
|
|
19
|
+
* here filled with `ToolSection`s; in protoAgent it's the per-tool value renderer. */
|
|
20
|
+
export const States: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<div style={{ maxWidth: 460 }}>
|
|
23
|
+
<ToolCardList>
|
|
24
|
+
<ToolCard
|
|
25
|
+
name="web_fetch"
|
|
26
|
+
status="done"
|
|
27
|
+
icon={<Globe />}
|
|
28
|
+
duration={1240}
|
|
29
|
+
defaultOpen
|
|
30
|
+
actions={
|
|
31
|
+
<button
|
|
32
|
+
type="button"
|
|
33
|
+
className="pl-btn pl-btn--ghost pl-btn--icon pl-btn--sm"
|
|
34
|
+
title="Re-run"
|
|
35
|
+
onClick={(e) => e.stopPropagation()}
|
|
36
|
+
>
|
|
37
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
38
|
+
<path d="M23 4v6h-6M1 20v-6h6" />
|
|
39
|
+
<path d="M3.5 9a9 9 0 0 1 14.9-3.4L23 10M1 14l4.6 4.4A9 9 0 0 0 20.5 15" />
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
<ToolSection label="input" copyText='{"url":"https://protolabs.studio"}'>
|
|
45
|
+
<Mono>{`{ "url": "https://protolabs.studio" }`}</Mono>
|
|
46
|
+
</ToolSection>
|
|
47
|
+
<ToolSection label="result" copyText="200 OK · 18kb">
|
|
48
|
+
<Mono>200 OK · 18kb · text/html</Mono>
|
|
49
|
+
</ToolSection>
|
|
50
|
+
</ToolCard>
|
|
51
|
+
<ToolCard name="web_search" status="running" duration={820}>
|
|
52
|
+
<ToolSection label="input">
|
|
53
|
+
<Mono>protoLabs design system</Mono>
|
|
54
|
+
</ToolSection>
|
|
55
|
+
</ToolCard>
|
|
56
|
+
<ToolCard name="calculator" status="error">
|
|
57
|
+
<ToolSection label="result">
|
|
58
|
+
<Mono>SyntaxError: unexpected token</Mono>
|
|
59
|
+
</ToolSection>
|
|
60
|
+
</ToolCard>
|
|
61
|
+
<ToolCard name="current_time" status="done" duration={12} />
|
|
62
|
+
</ToolCardList>
|
|
63
|
+
</div>
|
|
64
|
+
),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** Subagent `task` with nested child tool calls (the `parentId` grouping). */
|
|
68
|
+
export const Nested: Story = {
|
|
69
|
+
render: () => (
|
|
70
|
+
<div style={{ maxWidth: 460 }}>
|
|
71
|
+
<ToolCard
|
|
72
|
+
name="task"
|
|
73
|
+
status="done"
|
|
74
|
+
duration={4300}
|
|
75
|
+
nested={
|
|
76
|
+
<>
|
|
77
|
+
<ToolCard name="web_search" status="done" duration={910}>
|
|
78
|
+
<ToolSection label="result"><Mono>7 results</Mono></ToolSection>
|
|
79
|
+
</ToolCard>
|
|
80
|
+
<ToolCard name="web_fetch" status="done" duration={1320}>
|
|
81
|
+
<ToolSection label="result"><Mono>200 OK</Mono></ToolSection>
|
|
82
|
+
</ToolCard>
|
|
83
|
+
</>
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
<ToolSection label="input"><Mono>research the brand voice</Mono></ToolSection>
|
|
87
|
+
</ToolCard>
|
|
88
|
+
</div>
|
|
89
|
+
),
|
|
90
|
+
};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/* @protolabsai/ui — ToolCard (streamed tool-call disclosure). Ported from the
|
|
2
|
+
protoAgent console's tool-card family onto --pl-* tokens. The frame only — the
|
|
3
|
+
body is a host-filled slot (per-tool value rendering stays app-side). */
|
|
4
|
+
|
|
5
|
+
.pl-toolcard-list {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: var(--pl-space-2);
|
|
9
|
+
min-width: 0;
|
|
10
|
+
}
|
|
11
|
+
.pl-toolcard-group {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: 6px;
|
|
15
|
+
min-width: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.pl-toolcard {
|
|
19
|
+
min-width: 0;
|
|
20
|
+
max-width: 100%;
|
|
21
|
+
background: var(--pl-color-bg-raised);
|
|
22
|
+
border: var(--pl-border-width) solid var(--pl-color-border);
|
|
23
|
+
border-radius: var(--pl-radius);
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.pl-toolcard__head-row {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
}
|
|
31
|
+
.pl-toolcard__head {
|
|
32
|
+
flex: 1 1 auto;
|
|
33
|
+
min-width: 0;
|
|
34
|
+
display: flex;
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 7px;
|
|
37
|
+
padding: 6px 9px;
|
|
38
|
+
background: none;
|
|
39
|
+
border: none;
|
|
40
|
+
color: var(--pl-color-fg-muted);
|
|
41
|
+
font: inherit;
|
|
42
|
+
font-size: 12px;
|
|
43
|
+
text-align: left;
|
|
44
|
+
cursor: pointer;
|
|
45
|
+
}
|
|
46
|
+
.pl-toolcard__actions {
|
|
47
|
+
flex: none;
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
gap: 2px;
|
|
51
|
+
padding-right: 6px;
|
|
52
|
+
color: var(--pl-color-fg-muted);
|
|
53
|
+
}
|
|
54
|
+
.pl-toolcard__head:disabled {
|
|
55
|
+
cursor: default;
|
|
56
|
+
}
|
|
57
|
+
.pl-toolcard__head:hover:not(:disabled) .pl-toolcard__name {
|
|
58
|
+
color: var(--pl-color-fg);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.pl-toolcard__caret {
|
|
62
|
+
flex: none;
|
|
63
|
+
display: inline-flex;
|
|
64
|
+
color: var(--pl-color-fg-subtle);
|
|
65
|
+
transition: transform var(--pl-motion-fast) var(--pl-motion-ease);
|
|
66
|
+
}
|
|
67
|
+
.pl-toolcard__caret--open {
|
|
68
|
+
transform: rotate(90deg);
|
|
69
|
+
}
|
|
70
|
+
.pl-toolcard__caret--hidden {
|
|
71
|
+
visibility: hidden;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.pl-toolcard__icon {
|
|
75
|
+
flex: none;
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
color: var(--pl-color-fg-muted);
|
|
78
|
+
}
|
|
79
|
+
.pl-toolcard__icon svg {
|
|
80
|
+
width: 13px;
|
|
81
|
+
height: 13px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.pl-toolcard__name {
|
|
85
|
+
flex: 1 1 auto;
|
|
86
|
+
min-width: 0;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
text-overflow: ellipsis;
|
|
89
|
+
white-space: nowrap;
|
|
90
|
+
font-family: var(--pl-font-mono);
|
|
91
|
+
color: var(--pl-color-fg);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.pl-toolcard__dur {
|
|
95
|
+
flex: none;
|
|
96
|
+
font-family: var(--pl-font-mono);
|
|
97
|
+
font-size: 10px;
|
|
98
|
+
color: var(--pl-color-fg-muted);
|
|
99
|
+
font-variant-numeric: tabular-nums;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.pl-toolcard__status {
|
|
103
|
+
flex: none;
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
}
|
|
106
|
+
.pl-toolcard__status--done {
|
|
107
|
+
color: var(--pl-color-status-success);
|
|
108
|
+
}
|
|
109
|
+
.pl-toolcard__status--error {
|
|
110
|
+
color: var(--pl-color-status-error);
|
|
111
|
+
}
|
|
112
|
+
.pl-toolcard__status--running {
|
|
113
|
+
color: var(--pl-color-status-warning);
|
|
114
|
+
}
|
|
115
|
+
.pl-toolcard__spin {
|
|
116
|
+
animation: pl-toolcard-spin 0.8s linear infinite;
|
|
117
|
+
}
|
|
118
|
+
@keyframes pl-toolcard-spin {
|
|
119
|
+
to {
|
|
120
|
+
transform: rotate(360deg);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.pl-toolcard__body {
|
|
125
|
+
display: flex;
|
|
126
|
+
flex-direction: column;
|
|
127
|
+
gap: 8px;
|
|
128
|
+
padding: 8px 9px;
|
|
129
|
+
border-top: var(--pl-border-width) solid var(--pl-color-border);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* subagent `task` nesting — indented rail of child cards */
|
|
133
|
+
.pl-toolcard__children {
|
|
134
|
+
display: flex;
|
|
135
|
+
flex-direction: column;
|
|
136
|
+
gap: 6px;
|
|
137
|
+
min-width: 0;
|
|
138
|
+
margin-left: 14px;
|
|
139
|
+
padding-left: 10px;
|
|
140
|
+
border-left: 2px solid var(--pl-color-border);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* body section (input / result) */
|
|
144
|
+
.pl-toolcard__section {
|
|
145
|
+
display: flex;
|
|
146
|
+
flex-direction: column;
|
|
147
|
+
gap: 4px;
|
|
148
|
+
min-width: 0;
|
|
149
|
+
}
|
|
150
|
+
.pl-toolcard__section-head {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
justify-content: space-between;
|
|
154
|
+
gap: 8px;
|
|
155
|
+
}
|
|
156
|
+
.pl-toolcard__label {
|
|
157
|
+
font-family: var(--pl-font-mono);
|
|
158
|
+
font-size: 10px;
|
|
159
|
+
text-transform: uppercase;
|
|
160
|
+
letter-spacing: 0.04em;
|
|
161
|
+
color: var(--pl-color-fg-subtle);
|
|
162
|
+
}
|
|
163
|
+
.pl-toolcard__copy {
|
|
164
|
+
display: inline-flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
justify-content: center;
|
|
167
|
+
padding: 2px;
|
|
168
|
+
background: none;
|
|
169
|
+
border: none;
|
|
170
|
+
border-radius: var(--pl-radius);
|
|
171
|
+
color: var(--pl-color-fg-muted);
|
|
172
|
+
cursor: pointer;
|
|
173
|
+
transition:
|
|
174
|
+
background var(--pl-motion-fast) var(--pl-motion-ease),
|
|
175
|
+
color var(--pl-motion-fast) var(--pl-motion-ease);
|
|
176
|
+
}
|
|
177
|
+
.pl-toolcard__copy:hover {
|
|
178
|
+
color: var(--pl-color-fg);
|
|
179
|
+
background: var(--pl-color-bg-hover);
|
|
180
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { cx } from "./internal";
|
|
4
|
+
|
|
5
|
+
// The disclosure FRAME for a streamed tool call. Presentation only: the host owns
|
|
6
|
+
// the ToolCall data (name/status/duration come straight off the wire type) and fills
|
|
7
|
+
// the body slot with its own per-tool value renderers. The DS deliberately does NOT
|
|
8
|
+
// render tool output — that's domain logic (calculator equations, fetched-page
|
|
9
|
+
// previews, KV grids …) that stays app-side. See protoContent issue #187.
|
|
10
|
+
|
|
11
|
+
/** Mirrors the host's `ToolCall.status`. */
|
|
12
|
+
export type ToolCardStatus = "running" | "done" | "error";
|
|
13
|
+
|
|
14
|
+
/** Optional wrapper around a stream of ToolCards — a gapped column. */
|
|
15
|
+
export function ToolCardList({ children, className }: { children: ReactNode; className?: string }) {
|
|
16
|
+
return <div className={cx("pl-toolcard-list", className)}>{children}</div>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatDuration(ms: number): string {
|
|
20
|
+
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function StatusGlyph({ status }: { status: ToolCardStatus }) {
|
|
24
|
+
if (status === "running") {
|
|
25
|
+
return (
|
|
26
|
+
<span className="pl-toolcard__status pl-toolcard__status--running" aria-label="running">
|
|
27
|
+
<svg className="pl-toolcard__spin" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round">
|
|
28
|
+
<path d="M21 12a9 9 0 1 1-6.2-8.5" />
|
|
29
|
+
</svg>
|
|
30
|
+
</span>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (status === "error") {
|
|
34
|
+
return (
|
|
35
|
+
<span className="pl-toolcard__status pl-toolcard__status--error" aria-label="error">
|
|
36
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round">
|
|
37
|
+
<path d="M6 6l12 12M18 6L6 18" />
|
|
38
|
+
</svg>
|
|
39
|
+
</span>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return (
|
|
43
|
+
<span className="pl-toolcard__status pl-toolcard__status--done" aria-label="done">
|
|
44
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
|
45
|
+
<path d="M20 6L9 17l-5-5" />
|
|
46
|
+
</svg>
|
|
47
|
+
</span>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A single tool-call disclosure: a row with leading icon, name, duration, and a
|
|
52
|
+
* running→done/error status glyph, expanding to a body slot. Collapsed and STICKY
|
|
53
|
+
* by default — the open state is the user's explicit choice, so the message never
|
|
54
|
+
* reflows as tools start and finish. `status`/`name`/`duration` come off the host's
|
|
55
|
+
* ToolCall; `children` is the rendered input/result (compose `ToolSection`s);
|
|
56
|
+
* `nested` holds child cards for a subagent `task` (indented). */
|
|
57
|
+
export function ToolCard({
|
|
58
|
+
name,
|
|
59
|
+
status,
|
|
60
|
+
icon,
|
|
61
|
+
duration,
|
|
62
|
+
defaultOpen = false,
|
|
63
|
+
nested,
|
|
64
|
+
actions,
|
|
65
|
+
children,
|
|
66
|
+
className,
|
|
67
|
+
}: {
|
|
68
|
+
name: ReactNode;
|
|
69
|
+
status: ToolCardStatus;
|
|
70
|
+
/** Leading glyph — the host maps tool→icon (the DS stays icon-agnostic). */
|
|
71
|
+
icon?: ReactNode;
|
|
72
|
+
/** Elapsed ms (`ToolCall.durationMs`) — rendered "820ms" / "1.2s". */
|
|
73
|
+
duration?: number;
|
|
74
|
+
defaultOpen?: boolean;
|
|
75
|
+
/** Indented child tool cards for a subagent `task`. */
|
|
76
|
+
nested?: ReactNode;
|
|
77
|
+
/** Trailing header slot — hang a re-run / per-tool affordance here. Sits outside
|
|
78
|
+
* the disclosure toggle (so it can hold its own buttons). */
|
|
79
|
+
actions?: ReactNode;
|
|
80
|
+
/** Expanded body — the rendered input/result (compose `ToolSection`s). */
|
|
81
|
+
children?: ReactNode;
|
|
82
|
+
className?: string;
|
|
83
|
+
}) {
|
|
84
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
85
|
+
const hasBody = children != null;
|
|
86
|
+
const card = (
|
|
87
|
+
<div className={cx("pl-toolcard", `pl-toolcard--${status}`, className)}>
|
|
88
|
+
<div className="pl-toolcard__head-row">
|
|
89
|
+
<button
|
|
90
|
+
type="button"
|
|
91
|
+
className="pl-toolcard__head"
|
|
92
|
+
aria-expanded={hasBody ? open : undefined}
|
|
93
|
+
disabled={!hasBody}
|
|
94
|
+
onClick={() => setOpen((v) => !v)}
|
|
95
|
+
>
|
|
96
|
+
<span
|
|
97
|
+
className={cx(
|
|
98
|
+
"pl-toolcard__caret",
|
|
99
|
+
!hasBody && "pl-toolcard__caret--hidden",
|
|
100
|
+
open && "pl-toolcard__caret--open",
|
|
101
|
+
)}
|
|
102
|
+
aria-hidden
|
|
103
|
+
>
|
|
104
|
+
<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
|
105
|
+
<path d="M9 6l6 6-6 6" />
|
|
106
|
+
</svg>
|
|
107
|
+
</span>
|
|
108
|
+
{icon != null && (
|
|
109
|
+
<span className="pl-toolcard__icon" aria-hidden>
|
|
110
|
+
{icon}
|
|
111
|
+
</span>
|
|
112
|
+
)}
|
|
113
|
+
<span className="pl-toolcard__name">{name}</span>
|
|
114
|
+
{duration != null && <span className="pl-toolcard__dur">{formatDuration(duration)}</span>}
|
|
115
|
+
<StatusGlyph status={status} />
|
|
116
|
+
</button>
|
|
117
|
+
{actions != null && <div className="pl-toolcard__actions">{actions}</div>}
|
|
118
|
+
</div>
|
|
119
|
+
{hasBody && open && <div className="pl-toolcard__body">{children}</div>}
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
if (nested == null) return card;
|
|
123
|
+
return (
|
|
124
|
+
<div className="pl-toolcard-group">
|
|
125
|
+
{card}
|
|
126
|
+
<div className="pl-toolcard__children">{nested}</div>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function CopyButton({ text }: { text: string }) {
|
|
132
|
+
const [copied, setCopied] = useState(false);
|
|
133
|
+
return (
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
className="pl-toolcard__copy"
|
|
137
|
+
title="Copy to clipboard"
|
|
138
|
+
aria-label={copied ? "Copied" : "Copy"}
|
|
139
|
+
onClick={async () => {
|
|
140
|
+
try {
|
|
141
|
+
await navigator.clipboard.writeText(text);
|
|
142
|
+
setCopied(true);
|
|
143
|
+
setTimeout(() => setCopied(false), 1200);
|
|
144
|
+
} catch {
|
|
145
|
+
/* clipboard unavailable (insecure context / denied) */
|
|
146
|
+
}
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{copied ? (
|
|
150
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
|
|
151
|
+
<path d="M20 6L9 17l-5-5" />
|
|
152
|
+
</svg>
|
|
153
|
+
) : (
|
|
154
|
+
<svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
|
155
|
+
<rect x="9" y="9" width="13" height="13" rx="2" />
|
|
156
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
|
|
157
|
+
</svg>
|
|
158
|
+
)}
|
|
159
|
+
</button>
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A labelled body section with an optional copy affordance — e.g. input / result.
|
|
164
|
+
* Compose inside `ToolCard`'s body; fill `children` with the rendered value. */
|
|
165
|
+
export function ToolSection({
|
|
166
|
+
label,
|
|
167
|
+
copyText,
|
|
168
|
+
children,
|
|
169
|
+
}: {
|
|
170
|
+
label: ReactNode;
|
|
171
|
+
/** When set, renders a self-contained copy-to-clipboard button. */
|
|
172
|
+
copyText?: string;
|
|
173
|
+
children: ReactNode;
|
|
174
|
+
}) {
|
|
175
|
+
return (
|
|
176
|
+
<div className="pl-toolcard__section">
|
|
177
|
+
<div className="pl-toolcard__section-head">
|
|
178
|
+
<span className="pl-toolcard__label">{label}</span>
|
|
179
|
+
{copyText != null && <CopyButton text={copyText} />}
|
|
180
|
+
</div>
|
|
181
|
+
{children}
|
|
182
|
+
</div>
|
|
183
|
+
);
|
|
184
|
+
}
|