@neurocode-ai/session-ui 1.18.8

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 (114) hide show
  1. package/package.json +71 -0
  2. package/src/components/apply-patch-file.test.ts +43 -0
  3. package/src/components/apply-patch-file.ts +78 -0
  4. package/src/components/basic-tool.css +358 -0
  5. package/src/components/basic-tool.stories.tsx +133 -0
  6. package/src/components/basic-tool.tsx +343 -0
  7. package/src/components/dock-prompt.stories.tsx +62 -0
  8. package/src/components/dock-prompt.tsx +23 -0
  9. package/src/components/file-media.tsx +291 -0
  10. package/src/components/file-search.tsx +72 -0
  11. package/src/components/file-ssr.tsx +197 -0
  12. package/src/components/file.css +46 -0
  13. package/src/components/file.tsx +1202 -0
  14. package/src/components/line-comment-annotations.tsx +637 -0
  15. package/src/components/line-comment-styles.ts +292 -0
  16. package/src/components/line-comment.stories.tsx +115 -0
  17. package/src/components/line-comment.tsx +439 -0
  18. package/src/components/markdown-cache.tsx +78 -0
  19. package/src/components/markdown-code-state.test.ts +32 -0
  20. package/src/components/markdown-code-state.ts +22 -0
  21. package/src/components/markdown-inline-code-kind.test.ts +46 -0
  22. package/src/components/markdown-inline-code-kind.ts +1915 -0
  23. package/src/components/markdown-preload.test.ts +18 -0
  24. package/src/components/markdown-shiki.worker.ts +104 -0
  25. package/src/components/markdown-stream.test.ts +194 -0
  26. package/src/components/markdown-stream.ts +110 -0
  27. package/src/components/markdown-worker-protocol.test.ts +81 -0
  28. package/src/components/markdown-worker-protocol.ts +48 -0
  29. package/src/components/markdown-worker-queue.test.ts +49 -0
  30. package/src/components/markdown-worker-queue.ts +64 -0
  31. package/src/components/markdown-worker-transport.test.ts +56 -0
  32. package/src/components/markdown-worker-transport.ts +41 -0
  33. package/src/components/markdown-worker.ts +122 -0
  34. package/src/components/markdown.css +377 -0
  35. package/src/components/markdown.stories.tsx +53 -0
  36. package/src/components/markdown.tsx +678 -0
  37. package/src/components/message-file.test.ts +63 -0
  38. package/src/components/message-file.ts +34 -0
  39. package/src/components/message-nav.css +127 -0
  40. package/src/components/message-nav.stories.tsx +7 -0
  41. package/src/components/message-nav.tsx +102 -0
  42. package/src/components/message-part-text.ts +3 -0
  43. package/src/components/message-part.css +1569 -0
  44. package/src/components/message-part.stories.tsx +7 -0
  45. package/src/components/message-part.test.ts +28 -0
  46. package/src/components/message-part.tsx +2662 -0
  47. package/src/components/session-diff.test.ts +135 -0
  48. package/src/components/session-diff.ts +145 -0
  49. package/src/components/session-retry.tsx +74 -0
  50. package/src/components/session-review.css +247 -0
  51. package/src/components/session-review.stories.tsx +7 -0
  52. package/src/components/session-review.tsx +657 -0
  53. package/src/components/session-turn.css +241 -0
  54. package/src/components/session-turn.stories.tsx +7 -0
  55. package/src/components/session-turn.tsx +543 -0
  56. package/src/components/shell-submessage-motion.stories.tsx +346 -0
  57. package/src/components/shell-submessage.css +23 -0
  58. package/src/components/timeline-playground.stories.tsx +2090 -0
  59. package/src/components/tool-count-label.css +57 -0
  60. package/src/components/tool-count-label.tsx +58 -0
  61. package/src/components/tool-count-summary.css +102 -0
  62. package/src/components/tool-count-summary.stories.tsx +238 -0
  63. package/src/components/tool-count-summary.tsx +52 -0
  64. package/src/components/tool-error-card.css +91 -0
  65. package/src/components/tool-error-card.stories.tsx +92 -0
  66. package/src/components/tool-error-card.tsx +157 -0
  67. package/src/components/tool-status-title.css +89 -0
  68. package/src/components/tool-status-title.tsx +133 -0
  69. package/src/context/data.tsx +64 -0
  70. package/src/context/index.ts +1 -0
  71. package/src/pierre/comment-hover.ts +83 -0
  72. package/src/pierre/commented-lines.ts +91 -0
  73. package/src/pierre/diff-selection.ts +71 -0
  74. package/src/pierre/file-find.ts +485 -0
  75. package/src/pierre/file-runtime.ts +114 -0
  76. package/src/pierre/file-selection.ts +85 -0
  77. package/src/pierre/index.ts +186 -0
  78. package/src/pierre/media.ts +110 -0
  79. package/src/pierre/selection-bridge.ts +132 -0
  80. package/src/pierre/virtualizer.ts +100 -0
  81. package/src/pierre/worker.ts +52 -0
  82. package/src/styles/index.css +14 -0
  83. package/src/v2/components/attachment-card-v2.css +70 -0
  84. package/src/v2/components/attachment-card-v2.tsx +33 -0
  85. package/src/v2/components/basic-tool-v2.css +163 -0
  86. package/src/v2/components/basic-tool-v2.stories.tsx +137 -0
  87. package/src/v2/components/basic-tool-v2.tsx +139 -0
  88. package/src/v2/components/comment-card-v2.tsx +64 -0
  89. package/src/v2/components/line-comment-annotations-v2.tsx +220 -0
  90. package/src/v2/components/prompt-input/attachments.ts +266 -0
  91. package/src/v2/components/prompt-input/index.tsx +706 -0
  92. package/src/v2/components/prompt-input/interaction.ts +482 -0
  93. package/src/v2/components/prompt-input/machine.test.ts +164 -0
  94. package/src/v2/components/prompt-input/machine.ts +261 -0
  95. package/src/v2/components/prompt-input/prompt-input.stories.tsx +221 -0
  96. package/src/v2/components/prompt-input/store.test.ts +116 -0
  97. package/src/v2/components/prompt-input/store.ts +152 -0
  98. package/src/v2/components/prompt-input/types.ts +106 -0
  99. package/src/v2/components/session-file-panel-v2.tsx +43 -0
  100. package/src/v2/components/session-progress-indicator-v2.css +875 -0
  101. package/src/v2/components/session-progress-indicator-v2.stories.tsx +66 -0
  102. package/src/v2/components/session-progress-indicator-v2.tsx +32 -0
  103. package/src/v2/components/session-review-empty-changes-v2.tsx +17 -0
  104. package/src/v2/components/session-review-empty-no-git-v2.tsx +28 -0
  105. package/src/v2/components/session-review-file-preview-v2-virtualize.test.ts +13 -0
  106. package/src/v2/components/session-review-file-preview-v2-virtualize.ts +5 -0
  107. package/src/v2/components/session-review-file-preview-v2.tsx +290 -0
  108. package/src/v2/components/session-review-v2.css +458 -0
  109. package/src/v2/components/session-review-v2.tsx +340 -0
  110. package/src/v2/components/tool-error-card-v2.css +200 -0
  111. package/src/v2/components/tool-error-card-v2.stories.tsx +91 -0
  112. package/src/v2/components/tool-error-card-v2.tsx +166 -0
  113. package/sst-env.d.ts +10 -0
  114. package/tsconfig.json +19 -0
@@ -0,0 +1,57 @@
1
+ [data-component="tool-count-label"] {
2
+ display: inline-flex;
3
+ align-items: baseline;
4
+ white-space: nowrap;
5
+ gap: 0;
6
+
7
+ [data-slot="tool-count-label-before"] {
8
+ display: inline-block;
9
+ white-space: pre;
10
+ line-height: inherit;
11
+ }
12
+
13
+ [data-slot="tool-count-label-word"] {
14
+ display: inline-flex;
15
+ align-items: baseline;
16
+ white-space: pre;
17
+ line-height: inherit;
18
+ }
19
+
20
+ [data-slot="tool-count-label-stem"] {
21
+ display: inline-block;
22
+ white-space: pre;
23
+ }
24
+
25
+ [data-slot="tool-count-label-suffix"] {
26
+ display: inline-grid;
27
+ grid-template-columns: 0fr;
28
+ opacity: 0;
29
+ filter: blur(calc(var(--tool-motion-blur, 2px) * 0.42));
30
+ overflow: hidden;
31
+ transform: translateX(-0.04em);
32
+ transition-property: grid-template-columns, opacity, filter, transform;
33
+ transition-duration: 250ms, 250ms, 250ms, 250ms;
34
+ transition-timing-function:
35
+ var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), ease-out, ease-out,
36
+ var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1));
37
+ }
38
+
39
+ [data-slot="tool-count-label-suffix"][data-active="true"] {
40
+ grid-template-columns: 1fr;
41
+ opacity: 1;
42
+ filter: blur(0);
43
+ transform: translateX(0);
44
+ }
45
+
46
+ [data-slot="tool-count-label-suffix-inner"] {
47
+ min-width: 0;
48
+ overflow: hidden;
49
+ white-space: pre;
50
+ }
51
+ }
52
+
53
+ @media (prefers-reduced-motion: reduce) {
54
+ [data-component="tool-count-label"] [data-slot="tool-count-label-suffix"] {
55
+ transition-duration: 0ms;
56
+ }
57
+ }
@@ -0,0 +1,58 @@
1
+ import { createMemo } from "solid-js"
2
+ import { AnimatedNumber } from "@neurocode-ai/ui/animated-number"
3
+
4
+ function split(text: string) {
5
+ const match = /{{\s*count\s*}}/.exec(text)
6
+ if (!match) return { before: "", after: text }
7
+ if (match.index === undefined) return { before: "", after: text }
8
+ return {
9
+ before: text.slice(0, match.index),
10
+ after: text.slice(match.index + match[0].length),
11
+ }
12
+ }
13
+
14
+ function common(one: string, other: string) {
15
+ const a = Array.from(one)
16
+ const b = Array.from(other)
17
+ let i = 0
18
+ while (i < a.length && i < b.length && a[i] === b[i]) i++
19
+ return {
20
+ stem: a.slice(0, i).join(""),
21
+ one: a.slice(i).join(""),
22
+ other: b.slice(i).join(""),
23
+ }
24
+ }
25
+
26
+ export function AnimatedCountLabel(props: { count: number; one: string; other: string; class?: string }) {
27
+ const one = createMemo(() => split(props.one))
28
+ const other = createMemo(() => split(props.other))
29
+ const singular = createMemo(() => Math.round(props.count) === 1)
30
+ const active = createMemo(() => (singular() ? one() : other()))
31
+ const suffix = createMemo(() => common(one().after, other().after))
32
+ const splitSuffix = createMemo(
33
+ () =>
34
+ one().before === other().before &&
35
+ (one().after.startsWith(other().after) || other().after.startsWith(one().after)),
36
+ )
37
+ const before = createMemo(() => (splitSuffix() ? one().before : active().before))
38
+ const stem = createMemo(() => (splitSuffix() ? suffix().stem : active().after))
39
+ const tail = createMemo(() => {
40
+ if (!splitSuffix()) return ""
41
+ if (singular()) return suffix().one
42
+ return suffix().other
43
+ })
44
+ const showTail = createMemo(() => splitSuffix() && tail().length > 0)
45
+
46
+ return (
47
+ <span data-component="tool-count-label" class={props.class}>
48
+ <span data-slot="tool-count-label-before">{before()}</span>
49
+ <AnimatedNumber value={props.count} />
50
+ <span data-slot="tool-count-label-word">
51
+ <span data-slot="tool-count-label-stem">{stem()}</span>
52
+ <span data-slot="tool-count-label-suffix" data-active={showTail() ? "true" : "false"}>
53
+ <span data-slot="tool-count-label-suffix-inner">{tail()}</span>
54
+ </span>
55
+ </span>
56
+ </span>
57
+ )
58
+ }
@@ -0,0 +1,102 @@
1
+ [data-component="tool-count-summary"] {
2
+ display: inline-flex;
3
+ align-items: baseline;
4
+ white-space: nowrap;
5
+
6
+ [data-slot="tool-count-summary-empty"] {
7
+ display: inline-grid;
8
+ grid-template-columns: 1fr;
9
+ align-items: baseline;
10
+ opacity: 1;
11
+ filter: blur(0);
12
+ transform: translateY(0) scale(1);
13
+ overflow: hidden;
14
+ transform-origin: left center;
15
+ transition-property: grid-template-columns, opacity, filter, transform;
16
+ transition-duration:
17
+ var(--tool-motion-spring-ms, 480ms), var(--tool-motion-fade-ms, 240ms), var(--tool-motion-fade-ms, 280ms),
18
+ var(--tool-motion-spring-ms, 480ms);
19
+ transition-timing-function:
20
+ var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), ease-out, ease-out,
21
+ var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1));
22
+ }
23
+
24
+ [data-slot="tool-count-summary-empty"][data-active="false"] {
25
+ grid-template-columns: 0fr;
26
+ opacity: 0;
27
+ filter: blur(calc(var(--tool-motion-blur, 2px) * 0.72));
28
+ transform: translateY(0.05em) scale(0.985);
29
+ }
30
+
31
+ [data-slot="tool-count-summary-item"] {
32
+ display: inline-grid;
33
+ grid-template-columns: 0fr;
34
+ align-items: baseline;
35
+ opacity: 0;
36
+ filter: blur(var(--tool-motion-blur, 2px));
37
+ transform: translateY(0.06em) scale(0.985);
38
+ overflow: hidden;
39
+ transform-origin: left center;
40
+ transition-property: grid-template-columns, opacity, filter, transform;
41
+ transition-duration:
42
+ var(--tool-motion-spring-ms, 480ms), var(--tool-motion-fade-ms, 280ms), var(--tool-motion-fade-ms, 320ms),
43
+ var(--tool-motion-spring-ms, 480ms);
44
+ transition-timing-function:
45
+ var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1)), ease-out, ease-out,
46
+ var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1));
47
+ }
48
+
49
+ [data-slot="tool-count-summary-item"][data-active="true"] {
50
+ grid-template-columns: 1fr;
51
+ opacity: 1;
52
+ filter: blur(0);
53
+ transform: translateY(0) scale(1);
54
+ }
55
+
56
+ [data-slot="tool-count-summary-empty-inner"] {
57
+ min-width: 0;
58
+ overflow: hidden;
59
+ white-space: nowrap;
60
+ }
61
+
62
+ [data-slot="tool-count-summary-item-inner"] {
63
+ display: inline-flex;
64
+ align-items: baseline;
65
+ min-width: 0;
66
+ overflow: hidden;
67
+ white-space: nowrap;
68
+ }
69
+
70
+ [data-slot="tool-count-summary-prefix"] {
71
+ display: inline-flex;
72
+ align-items: baseline;
73
+ justify-content: flex-start;
74
+ max-width: 0;
75
+ margin-right: 0;
76
+ opacity: 0;
77
+ filter: blur(calc(var(--tool-motion-blur, 2px) * 0.55));
78
+ overflow: hidden;
79
+ transform: translateX(-0.08em);
80
+ transition-property: opacity, filter, transform;
81
+ transition-duration:
82
+ calc(var(--tool-motion-fade-ms, 200ms) * 0.75), calc(var(--tool-motion-fade-ms, 220ms) * 0.75),
83
+ calc(var(--tool-motion-fade-ms, 220ms) * 0.6);
84
+ transition-timing-function: ease-out, ease-out, ease-out;
85
+ }
86
+
87
+ [data-slot="tool-count-summary-prefix"][data-active="true"] {
88
+ max-width: 1ch;
89
+ margin-right: 0.45ch;
90
+ opacity: 1;
91
+ filter: blur(0);
92
+ transform: translateX(0);
93
+ }
94
+ }
95
+
96
+ @media (prefers-reduced-motion: reduce) {
97
+ [data-component="tool-count-summary"] [data-slot="tool-count-summary-empty"],
98
+ [data-component="tool-count-summary"] [data-slot="tool-count-summary-item"],
99
+ [data-component="tool-count-summary"] [data-slot="tool-count-summary-prefix"] {
100
+ transition-duration: 0ms;
101
+ }
102
+ }
@@ -0,0 +1,238 @@
1
+ // @ts-nocheck
2
+ import { onCleanup } from "solid-js"
3
+ import { createStore } from "solid-js/store"
4
+ import { AnimatedCountList, type CountItem } from "./tool-count-summary"
5
+ import { ToolStatusTitle } from "./tool-status-title"
6
+
7
+ export default {
8
+ title: "UI/AnimatedCountList",
9
+ id: "components-animated-count-list",
10
+ tags: ["autodocs"],
11
+ parameters: {
12
+ docs: {
13
+ description: {
14
+ component: `### Overview
15
+ Animated count list that smoothly transitions items in/out as counts change.
16
+
17
+ Uses \`grid-template-columns: 0fr → 1fr\` for width animations and the odometer
18
+ digit roller for count transitions. Shown here with \`ToolStatusTitle\` exactly
19
+ as it appears in the context tool group on the session page.`,
20
+ },
21
+ },
22
+ },
23
+ }
24
+
25
+ const TEXT = {
26
+ active: "Exploring",
27
+ done: "Explored",
28
+ read: { one: "{{count}} read", other: "{{count}} reads" },
29
+ search: { one: "{{count}} search", other: "{{count}} searches" },
30
+ list: { one: "{{count}} list", other: "{{count}} lists" },
31
+ } as const
32
+
33
+ function rand(min: number, max: number) {
34
+ return Math.floor(Math.random() * (max - min + 1)) + min
35
+ }
36
+
37
+ const btn = (accent?: boolean) =>
38
+ ({
39
+ padding: "6px 14px",
40
+ "border-radius": "6px",
41
+ border: "1px solid var(--color-divider, #333)",
42
+ background: accent ? "var(--color-danger-fill, #c33)" : "var(--color-fill-element, #222)",
43
+ color: "var(--color-text, #eee)",
44
+ cursor: "pointer",
45
+ "font-size": "13px",
46
+ }) as const
47
+
48
+ const smallBtn = (active?: boolean) =>
49
+ ({
50
+ padding: "4px 12px",
51
+ "border-radius": "6px",
52
+ border: active ? "1px solid var(--color-accent, #58f)" : "1px solid var(--color-divider, #333)",
53
+ background: active ? "var(--color-accent, #58f)" : "var(--color-fill-element, #222)",
54
+ color: "var(--color-text, #eee)",
55
+ cursor: "pointer",
56
+ "font-size": "12px",
57
+ }) as const
58
+
59
+ export const Playground = {
60
+ render: () => {
61
+ const [state, setState] = createStore({
62
+ reads: 0,
63
+ searches: 0,
64
+ lists: 0,
65
+ active: false,
66
+ reducedMotion: false,
67
+ })
68
+ const reads = () => state.reads
69
+ const searches = () => state.searches
70
+ const lists = () => state.lists
71
+ const active = () => state.active
72
+ const reducedMotion = () => state.reducedMotion
73
+
74
+ let timeouts: ReturnType<typeof setTimeout>[] = []
75
+
76
+ const clearAll = () => {
77
+ for (const t of timeouts) clearTimeout(t)
78
+ timeouts = []
79
+ }
80
+
81
+ onCleanup(clearAll)
82
+
83
+ const startSim = () => {
84
+ clearAll()
85
+ setState("reads", 0)
86
+ setState("searches", 0)
87
+ setState("lists", 0)
88
+ setState("active", true)
89
+ const steps = rand(3, 10)
90
+ let elapsed = 0
91
+
92
+ for (let i = 0; i < steps; i++) {
93
+ const delay = rand(300, 800)
94
+ elapsed += delay
95
+ const t = setTimeout(() => {
96
+ const pick = rand(0, 2)
97
+ if (pick === 0) setState("reads", (value) => value + 1)
98
+ else if (pick === 1) setState("searches", (value) => value + 1)
99
+ else setState("lists", (value) => value + 1)
100
+ }, elapsed)
101
+ timeouts.push(t)
102
+ }
103
+
104
+ const end = setTimeout(() => setState("active", false), elapsed + 100)
105
+ timeouts.push(end)
106
+ }
107
+
108
+ const stopSim = () => {
109
+ clearAll()
110
+ setState("active", false)
111
+ }
112
+
113
+ const reset = () => {
114
+ stopSim()
115
+ setState("reads", 0)
116
+ setState("searches", 0)
117
+ setState("lists", 0)
118
+ }
119
+
120
+ const items = (): CountItem[] => [
121
+ { key: "read", count: reads(), one: TEXT.read.one, other: TEXT.read.other },
122
+ { key: "search", count: searches(), one: TEXT.search.one, other: TEXT.search.other },
123
+ { key: "list", count: lists(), one: TEXT.list.one, other: TEXT.list.other },
124
+ ]
125
+
126
+ return (
127
+ <div style={{ display: "grid", gap: "24px", padding: "20px", "max-width": "520px" }}>
128
+ {reducedMotion() && (
129
+ <style>
130
+ {`[data-reduced-motion="true"] *,
131
+ [data-reduced-motion="true"] *::before,
132
+ [data-reduced-motion="true"] *::after {
133
+ transition-duration: 0ms !important;
134
+ }`}
135
+ </style>
136
+ )}
137
+
138
+ {/* Matches context-tool-group-trigger layout from message-part.tsx */}
139
+ <span
140
+ data-reduced-motion={reducedMotion()}
141
+ style={{
142
+ display: "flex",
143
+ "align-items": "center",
144
+ gap: "8px",
145
+ "font-size": "14px",
146
+ "font-weight": "500",
147
+ color: "var(--text-strong, #eee)",
148
+ "min-width": "0",
149
+ }}
150
+ >
151
+ <span style={{ "flex-shrink": "0" }}>
152
+ <ToolStatusTitle active={active()} activeText={TEXT.active} doneText={TEXT.done} split={false} />
153
+ </span>
154
+ <span
155
+ style={{
156
+ "min-width": "0",
157
+ overflow: "hidden",
158
+ "text-overflow": "ellipsis",
159
+ "white-space": "nowrap",
160
+ "font-weight": "400",
161
+ color: "var(--text-base, #ccc)",
162
+ }}
163
+ >
164
+ <AnimatedCountList items={items()} fallback="" />
165
+ </span>
166
+ </span>
167
+
168
+ <div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
169
+ <button onClick={() => (active() ? stopSim() : startSim())} style={btn(active())}>
170
+ {active() ? "Stop" : "Simulate"}
171
+ </button>
172
+ <button onClick={reset} style={btn()}>
173
+ Reset
174
+ </button>
175
+ <button onClick={() => setState("reducedMotion", (value) => !value)} style={smallBtn(reducedMotion())}>
176
+ {reducedMotion() ? "Motion: reduced" : "Motion: normal"}
177
+ </button>
178
+ </div>
179
+
180
+ <div style={{ display: "flex", gap: "8px", "flex-wrap": "wrap" }}>
181
+ <button onClick={() => setState("reads", (value) => value + 1)} style={smallBtn()}>
182
+ + read
183
+ </button>
184
+ <button onClick={() => setState("searches", (value) => value + 1)} style={smallBtn()}>
185
+ + search
186
+ </button>
187
+ <button onClick={() => setState("lists", (value) => value + 1)} style={smallBtn()}>
188
+ + list
189
+ </button>
190
+ </div>
191
+
192
+ <div
193
+ style={{
194
+ "font-size": "11px",
195
+ color: "var(--color-text-weak, #888)",
196
+ "font-family": "monospace",
197
+ }}
198
+ >
199
+ motion: {reducedMotion() ? "reduced" : "normal"} · active: {active() ? "true" : "false"} · reads: {reads()} ·
200
+ searches: {searches()} · lists: {lists()}
201
+ </div>
202
+ </div>
203
+ )
204
+ },
205
+ }
206
+
207
+ export const Empty = {
208
+ render: () => (
209
+ <span style={{ display: "flex", "align-items": "center", gap: "8px", "font-size": "14px", "font-weight": "500" }}>
210
+ <ToolStatusTitle active activeText="Exploring" doneText="Explored" split={false} />
211
+ <AnimatedCountList
212
+ items={[
213
+ { key: "read", count: 0, one: "{{count}} read", other: "{{count}} reads" },
214
+ { key: "search", count: 0, one: "{{count}} search", other: "{{count}} searches" },
215
+ ]}
216
+ fallback=""
217
+ />
218
+ </span>
219
+ ),
220
+ }
221
+
222
+ export const Done = {
223
+ render: () => (
224
+ <span style={{ display: "flex", "align-items": "center", gap: "8px", "font-size": "14px", "font-weight": "500" }}>
225
+ <ToolStatusTitle active={false} activeText="Exploring" doneText="Explored" split={false} />
226
+ <span style={{ "font-weight": "400", color: "var(--text-base, #ccc)" }}>
227
+ <AnimatedCountList
228
+ items={[
229
+ { key: "read", count: 5, one: "{{count}} read", other: "{{count}} reads" },
230
+ { key: "search", count: 3, one: "{{count}} search", other: "{{count}} searches" },
231
+ { key: "list", count: 1, one: "{{count}} list", other: "{{count}} lists" },
232
+ ]}
233
+ fallback=""
234
+ />
235
+ </span>
236
+ </span>
237
+ ),
238
+ }
@@ -0,0 +1,52 @@
1
+ import { Index, createMemo } from "solid-js"
2
+ import { AnimatedCountLabel } from "./tool-count-label"
3
+
4
+ export type CountItem = {
5
+ key: string
6
+ count: number
7
+ one: string
8
+ other: string
9
+ }
10
+
11
+ export function AnimatedCountList(props: { items: CountItem[]; fallback?: string; class?: string }) {
12
+ const visible = createMemo(() => props.items.filter((item) => item.count > 0))
13
+ const fallback = createMemo(() => props.fallback ?? "")
14
+ const showEmpty = createMemo(() => visible().length === 0 && fallback().length > 0)
15
+
16
+ return (
17
+ <span data-component="tool-count-summary" class={props.class}>
18
+ <span data-slot="tool-count-summary-empty" data-active={showEmpty() ? "true" : "false"}>
19
+ <span data-slot="tool-count-summary-empty-inner">{fallback()}</span>
20
+ </span>
21
+
22
+ <Index each={props.items}>
23
+ {(item, index) => {
24
+ const active = createMemo(() => item().count > 0)
25
+ const hasPrev = createMemo(() => {
26
+ for (let i = index - 1; i >= 0; i--) {
27
+ if (props.items[i].count > 0) return true
28
+ }
29
+ return false
30
+ })
31
+
32
+ return (
33
+ <>
34
+ <span data-slot="tool-count-summary-prefix" data-active={active() && hasPrev() ? "true" : "false"}>
35
+ ,
36
+ </span>
37
+ <span data-slot="tool-count-summary-item" data-active={active() ? "true" : "false"}>
38
+ <span data-slot="tool-count-summary-item-inner">
39
+ <AnimatedCountLabel
40
+ one={item().one}
41
+ other={item().other}
42
+ count={Math.max(0, Math.round(item().count))}
43
+ />
44
+ </span>
45
+ </span>
46
+ </>
47
+ )
48
+ }}
49
+ </Index>
50
+ </span>
51
+ )
52
+ }
@@ -0,0 +1,91 @@
1
+ [data-component="card"][data-kind="tool-error-card"] {
2
+ --card-pad-y: 8px;
3
+ --card-line-pad: 12px;
4
+
5
+ [data-slot="basic-tool-tool-title"] {
6
+ color: var(--v2-text-text-base);
7
+ }
8
+
9
+ [data-slot="basic-tool-tool-subtitle"] {
10
+ color: var(--v2-text-text-muted);
11
+ }
12
+
13
+ [data-slot="collapsible-arrow"],
14
+ [data-slot="collapsible-arrow-icon"] {
15
+ color: var(--v2-text-text-faint);
16
+ }
17
+
18
+ > [data-component="collapsible"].tool-collapsible {
19
+ gap: 0px;
20
+ }
21
+
22
+ > [data-component="collapsible"].tool-collapsible[data-open="true"] {
23
+ gap: 4px;
24
+ }
25
+
26
+ [data-component="tool-error-card-icon"] [data-component="icon"] {
27
+ color: var(--card-accent);
28
+ }
29
+
30
+ [data-slot="tool-error-card-content"] {
31
+ position: relative;
32
+ padding-left: 24px;
33
+ margin-bottom: 8px;
34
+ -webkit-user-select: text;
35
+ user-select: text;
36
+ }
37
+
38
+ > [data-component="collapsible"].tool-collapsible[data-open="true"] [data-slot="tool-error-card-content"] {
39
+ padding-right: 40px;
40
+ }
41
+
42
+ [data-slot="tool-error-card-copy"] {
43
+ position: absolute;
44
+ top: 0;
45
+ right: 0;
46
+ opacity: 0;
47
+ pointer-events: none;
48
+ transition: opacity 0.15s ease;
49
+ will-change: opacity;
50
+ }
51
+
52
+ &:hover [data-slot="tool-error-card-copy"],
53
+ &:focus-within [data-slot="tool-error-card-copy"] {
54
+ opacity: 1;
55
+ pointer-events: auto;
56
+ }
57
+
58
+ [data-slot="tool-error-card-content"] :where(*)::selection {
59
+ background: var(--v2-state-bg-danger);
60
+ color: var(--v2-text-text-base);
61
+ }
62
+
63
+ [data-slot="tool-error-card-content"] :where(*)::-moz-selection {
64
+ background: var(--v2-state-bg-danger);
65
+ color: var(--v2-text-text-base);
66
+ }
67
+ }
68
+
69
+ body:not([data-new-layout]) [data-component="card"][data-kind="tool-error-card"] {
70
+ [data-slot="basic-tool-tool-title"] {
71
+ color: var(--text-strong);
72
+ }
73
+
74
+ [data-slot="basic-tool-tool-subtitle"] {
75
+ color: var(--text-base);
76
+ }
77
+
78
+ [data-slot="collapsible-arrow"] {
79
+ color: var(--text-base);
80
+ }
81
+
82
+ [data-slot="collapsible-arrow-icon"] {
83
+ color: var(--icon-weaker);
84
+ }
85
+
86
+ [data-slot="tool-error-card-content"] :where(*)::selection,
87
+ [data-slot="tool-error-card-content"] :where(*)::-moz-selection {
88
+ background: var(--surface-critical-base);
89
+ color: var(--text-on-critical-base);
90
+ }
91
+ }
@@ -0,0 +1,92 @@
1
+ // @ts-nocheck
2
+ import { ToolErrorCard } from "./tool-error-card"
3
+
4
+ const docs = `### Overview
5
+ Tool call failure summary styled like a tool trigger.
6
+
7
+ ### API
8
+ - Required: \`tool\` (tool id, e.g. apply_patch, bash)
9
+ - Required: \`error\` (error string)
10
+
11
+ ### Behavior
12
+ - Collapsible; click header to expand/collapse.
13
+ `
14
+
15
+ const samples = [
16
+ {
17
+ tool: "apply_patch",
18
+ error:
19
+ "apply_patch verification failed: Failed to find expected lines in /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/session-turn.tsx",
20
+ },
21
+ {
22
+ tool: "bash",
23
+ error: "bash Command failed: exit code 1: bun test --watch",
24
+ },
25
+ {
26
+ tool: "read",
27
+ error:
28
+ "read File not found: /Users/davidhill/Documents/Local/opencode/packages/ui/src/components/does-not-exist.tsx",
29
+ },
30
+ {
31
+ tool: "glob",
32
+ error: "glob Pattern error: Invalid glob pattern: **/*[",
33
+ },
34
+ {
35
+ tool: "grep",
36
+ error: "grep Regex error: Invalid regular expression: (unterminated group",
37
+ },
38
+ {
39
+ tool: "webfetch",
40
+ error: "webfetch Request failed: 502 Bad Gateway",
41
+ },
42
+ {
43
+ tool: "websearch",
44
+ error: "websearch Rate limited: Please try again in 30 seconds",
45
+ },
46
+ {
47
+ tool: "question",
48
+ error: "question Dismissed: user dismissed this question",
49
+ },
50
+ ]
51
+
52
+ export default {
53
+ title: "UI/ToolErrorCard",
54
+ id: "components-tool-error-card",
55
+ component: ToolErrorCard,
56
+ tags: ["autodocs"],
57
+ parameters: {
58
+ docs: {
59
+ description: {
60
+ component: docs,
61
+ },
62
+ },
63
+ },
64
+ args: {
65
+ tool: "apply_patch",
66
+ error: samples[0].error,
67
+ },
68
+ argTypes: {
69
+ tool: {
70
+ control: "select",
71
+ options: ["apply_patch", "bash", "read", "glob", "grep", "webfetch", "websearch", "question"],
72
+ },
73
+ error: {
74
+ control: "text",
75
+ },
76
+ },
77
+ render: (props: { tool: string; error: string }) => {
78
+ return <ToolErrorCard tool={props.tool} error={props.error} />
79
+ },
80
+ }
81
+
82
+ export const All = {
83
+ render: () => {
84
+ return (
85
+ <div style="display: flex; flex-direction: column; gap: 12px; max-width: 720px;">
86
+ {samples.map((item) => (
87
+ <ToolErrorCard tool={item.tool} error={item.error} />
88
+ ))}
89
+ </div>
90
+ )
91
+ },
92
+ }