@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,340 @@
1
+ import { IconButton } from "@neurocode-ai/ui/icon-button"
2
+ import { useI18n } from "@neurocode-ai/ui/context/i18n"
3
+ import { SegmentedControlItemV2, SegmentedControlV2 } from "@neurocode-ai/ui/v2/segmented-control-v2"
4
+ import { TextInputV2 } from "@neurocode-ai/ui/v2/text-input-v2"
5
+ import { KeybindV2 } from "@neurocode-ai/ui/v2/keybind-v2"
6
+ import { Icon } from "@neurocode-ai/ui/v2/icon"
7
+ import { IconButtonV2 } from "@neurocode-ai/ui/v2/icon-button-v2"
8
+ import { TooltipV2 } from "@neurocode-ai/ui/v2/tooltip-v2"
9
+ import type { SessionReviewDiffStyle } from "../../components/session-review"
10
+ import { ResizeHandle } from "@neurocode-ai/ui/resize-handle"
11
+ import { ScrollView } from "@neurocode-ai/ui/scroll-view"
12
+ import { makeEventListener } from "@solid-primitives/event-listener"
13
+ import { Show, createEffect, createMemo, createSignal, type JSX } from "solid-js"
14
+ import { getWorkerPool } from "../../pierre/worker"
15
+ import { SessionFilePanelV2, SessionFilePanelV2Empty } from "./session-file-panel-v2"
16
+
17
+ export const SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT = 240
18
+ export const SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN = 200
19
+ export const SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX = 480
20
+
21
+ export type SessionReviewExpandMode = "expand" | "collapse"
22
+
23
+ export type SessionReviewV2Props = {
24
+ title?: JSX.Element
25
+ stats?: JSX.Element
26
+ empty?: JSX.Element
27
+ sidebarOpen?: boolean
28
+ sidebar?: JSX.Element
29
+ activeFile?: string
30
+ files: string[]
31
+ onSelectFile: (file: string) => void
32
+ diffStyle: SessionReviewDiffStyle
33
+ onDiffStyleChange?: (style: SessionReviewDiffStyle) => void
34
+ expandMode: SessionReviewExpandMode
35
+ onExpandModeChange: (mode: SessionReviewExpandMode) => void
36
+ preview?: JSX.Element
37
+ hasDiffs: boolean
38
+ }
39
+
40
+ export type SessionReviewV2SidebarProps = {
41
+ open: boolean
42
+ transition: boolean
43
+ title?: JSX.Element
44
+ stats?: JSX.Element
45
+ filter: string
46
+ onFilterChange: (value: string) => void
47
+ onFilterKeyDown?: JSX.EventHandlerUnion<HTMLInputElement, KeyboardEvent>
48
+ filterAutofocus?: boolean
49
+ filterRef?: (element: HTMLInputElement) => void
50
+ filterControls?: string
51
+ filterActiveDescendant?: string
52
+ filterExpanded?: boolean
53
+ width?: number
54
+ onWidthChange?: (width: number) => void
55
+ minWidth?: number
56
+ maxWidth?: number
57
+ viewportRef?: (element: HTMLDivElement) => void
58
+ children?: JSX.Element
59
+ }
60
+
61
+ export function SessionReviewV2Sidebar(props: SessionReviewV2SidebarProps) {
62
+ const i18n = useI18n()
63
+ const [resizing, setResizing] = createSignal(false)
64
+ const width = () => props.width ?? SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT
65
+ const minWidth = () => props.minWidth ?? SESSION_REVIEW_V2_SIDEBAR_WIDTH_MIN
66
+ const maxWidth = () => props.maxWidth ?? SESSION_REVIEW_V2_SIDEBAR_WIDTH_MAX
67
+
68
+ createEffect(() => {
69
+ if (!resizing()) return
70
+ const stop = () => setResizing(false)
71
+ makeEventListener(document, "pointerup", stop)
72
+ makeEventListener(document, "pointercancel", stop)
73
+ })
74
+
75
+ return (
76
+ <div data-component="session-review-v2-sidebar-root">
77
+ <Show when={props.open}>
78
+ <aside
79
+ data-slot="session-review-v2-sidebar"
80
+ data-transition={props.transition ? "" : undefined}
81
+ data-resizing={resizing() ? "" : undefined}
82
+ style={{ width: `${width()}px` }}
83
+ >
84
+ <div data-slot="session-review-v2-sidebar-header">
85
+ <div data-slot="session-review-v2-sidebar-title">{props.title}</div>
86
+ {props.stats}
87
+ </div>
88
+ <div data-slot="session-review-v2-sidebar-filter">
89
+ <TextInputV2
90
+ type="search"
91
+ value={props.filter}
92
+ onInput={(event) => props.onFilterChange(event.currentTarget.value)}
93
+ onKeyDown={props.onFilterKeyDown}
94
+ autofocus={props.filterAutofocus}
95
+ ref={props.filterRef}
96
+ role={props.filterControls ? "combobox" : undefined}
97
+ aria-autocomplete={props.filterControls ? "list" : undefined}
98
+ aria-controls={props.filterControls}
99
+ aria-activedescendant={props.filterActiveDescendant}
100
+ aria-expanded={props.filterControls ? props.filterExpanded : undefined}
101
+ showClearButton={props.filter.length > 0}
102
+ clearLabel={i18n.t("ui.list.clearFilter")}
103
+ onClearClick={() => props.onFilterChange("")}
104
+ placeholder={i18n.t("ui.sessionReviewV2.filterFiles")}
105
+ aria-label={i18n.t("ui.sessionReviewV2.filterFiles")}
106
+ leadingIcon={
107
+ <svg
108
+ width="14"
109
+ height="14"
110
+ viewBox="0 0 14 14"
111
+ fill="none"
112
+ xmlns="http://www.w3.org/2000/svg"
113
+ aria-hidden="true"
114
+ >
115
+ <path
116
+ d="M12.25 12.25L10.0625 10.0625M11.0833 6.41667C11.0833 8.994 8.994 11.0833 6.41667 11.0833C3.83934 11.0833 1.75 8.994 1.75 6.41667C1.75 3.83934 3.83934 1.75 6.41667 1.75C8.994 1.75 11.0833 3.83934 11.0833 6.41667Z"
117
+ stroke="currentColor"
118
+ stroke-linecap="square"
119
+ />
120
+ </svg>
121
+ }
122
+ />
123
+ </div>
124
+ <ScrollView
125
+ data-slot="session-review-v2-sidebar-tree"
126
+ class="group/file-tree-v2"
127
+ thumbVisibility="scroll"
128
+ viewportRef={props.viewportRef}
129
+ >
130
+ {props.children}
131
+ </ScrollView>
132
+ </aside>
133
+ </Show>
134
+ <Show when={props.open && props.onWidthChange}>
135
+ <div data-slot="session-review-v2-sidebar-resize" onPointerDown={() => setResizing(true)}>
136
+ <ResizeHandle
137
+ direction="horizontal"
138
+ size={width()}
139
+ min={minWidth()}
140
+ max={maxWidth()}
141
+ onResize={(next) => props.onWidthChange?.(next)}
142
+ onDblClick={() => props.onWidthChange?.(SESSION_REVIEW_V2_SIDEBAR_WIDTH_DEFAULT)}
143
+ />
144
+ </div>
145
+ </Show>
146
+ </div>
147
+ )
148
+ }
149
+
150
+ export function SessionReviewV2(props: SessionReviewV2Props) {
151
+ const i18n = useI18n()
152
+
153
+ createEffect(() => {
154
+ getWorkerPool(props.diffStyle)
155
+ })
156
+
157
+ const fileIndex = () => {
158
+ const files = props.files
159
+ if (files.length === 0) return -1
160
+
161
+ const active = props.activeFile
162
+ const i = active ? files.indexOf(active) : -1
163
+ if (i >= 0) return i
164
+ return 0
165
+ }
166
+
167
+ const prev = () => {
168
+ return props.files[fileIndex() - 1]
169
+ }
170
+
171
+ const next = () => {
172
+ return props.files[fileIndex() + 1]
173
+ }
174
+
175
+ const canCycle = () => props.files.length > 0
176
+ const showCollapsedMeta = () => props.sidebarOpen === false
177
+ // Memoize slot getters so Show conditions do not instantiate throwaway elements.
178
+ const title = createMemo(() => props.title)
179
+ const stats = createMemo(() => props.stats)
180
+
181
+ const cycle = (file: string | undefined) => {
182
+ if (!file) return
183
+ props.onSelectFile(file)
184
+ }
185
+
186
+ // The prev/next tooltips advertise < and >; keep the keys working while the
187
+ // pane is mounted, but never while typing in an input or comment editor.
188
+ makeEventListener(document, "keydown", (event) => {
189
+ if (event.defaultPrevented || event.ctrlKey || event.metaKey || event.altKey) return
190
+ if (event.key !== "<" && event.key !== ">") return
191
+ const target = event.target
192
+ if (target instanceof HTMLElement && (target.isContentEditable || target.closest("input, textarea, select"))) return
193
+ if (!props.hasDiffs || !canCycle()) return
194
+ const file = event.key === "<" ? prev() : next()
195
+ if (!file) return
196
+ event.preventDefault()
197
+ cycle(file)
198
+ })
199
+
200
+ const toolbarStart = () => (
201
+ <>
202
+ <Show when={showCollapsedMeta()}>
203
+ <div data-slot="session-review-v2-toolbar-collapsed-meta">
204
+ <Show when={title()}>
205
+ <div data-slot="session-review-v2-toolbar-title">{title()}</div>
206
+ </Show>
207
+ {stats()}
208
+ <Show when={canCycle()}>
209
+ <span data-slot="session-review-v2-file-position">
210
+ {fileIndex() + 1}/{props.files.length}
211
+ </span>
212
+ </Show>
213
+ </div>
214
+ </Show>
215
+ <div class="flex items-center">
216
+ <TooltipV2
217
+ openDelay={2000}
218
+ inactive={!prev()}
219
+ value={
220
+ <>
221
+ {i18n.t("ui.sessionReviewV2.previousFile")}
222
+ <KeybindV2 keys={["<"]} variant="neutral" />
223
+ </>
224
+ }
225
+ >
226
+ <IconButton
227
+ icon="arrow-left"
228
+ variant="ghost"
229
+ size="small"
230
+ class="session-review-v2-file-nav-button"
231
+ disabled={!prev()}
232
+ onClick={() => cycle(prev())}
233
+ aria-label={i18n.t("ui.sessionReviewV2.previousFile")}
234
+ />
235
+ </TooltipV2>
236
+ <TooltipV2
237
+ openDelay={2000}
238
+ inactive={!next()}
239
+ value={
240
+ <>
241
+ {i18n.t("ui.sessionReviewV2.nextFile")}
242
+ <KeybindV2 keys={[">"]} variant="neutral" />
243
+ </>
244
+ }
245
+ >
246
+ <IconButton
247
+ icon="arrow-right"
248
+ variant="ghost"
249
+ size="small"
250
+ class="session-review-v2-file-nav-button"
251
+ disabled={!next()}
252
+ onClick={() => cycle(next())}
253
+ aria-label={i18n.t("ui.sessionReviewV2.nextFile")}
254
+ />
255
+ </TooltipV2>
256
+ </div>
257
+ </>
258
+ )
259
+
260
+ const toolbarEnd = () => (
261
+ <>
262
+ <SegmentedControlV2
263
+ value={props.expandMode}
264
+ onChange={(value) => {
265
+ if (value !== "expand" && value !== "collapse") return
266
+ props.onExpandModeChange(value)
267
+ }}
268
+ class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
269
+ aria-label={i18n.t("ui.sessionReviewV2.expandMode")}
270
+ >
271
+ <TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.showAllLines")}>
272
+ <SegmentedControlItemV2 value="expand" aria-label={i18n.t("ui.sessionReviewV2.showAllLines")}>
273
+ <Icon name="expand" />
274
+ </SegmentedControlItemV2>
275
+ </TooltipV2>
276
+ <TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
277
+ <SegmentedControlItemV2 value="collapse" aria-label={i18n.t("ui.sessionReviewV2.hideNonDiffLines")}>
278
+ <Icon name="collapse" />
279
+ </SegmentedControlItemV2>
280
+ </TooltipV2>
281
+ </SegmentedControlV2>
282
+ <Show when={props.onDiffStyleChange}>
283
+ <SegmentedControlV2
284
+ value={props.diffStyle}
285
+ onChange={(value) => {
286
+ if (value !== "unified" && value !== "split") return
287
+ props.onDiffStyleChange?.(value)
288
+ }}
289
+ class="session-review-v2-segmented-control session-review-v2-segmented-control--icon"
290
+ aria-label={i18n.t("ui.sessionReviewV2.diffView")}
291
+ >
292
+ <TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
293
+ <SegmentedControlItemV2 value="unified" aria-label={i18n.t("ui.sessionReviewV2.unifiedDiff")}>
294
+ <Icon name="unified" />
295
+ </SegmentedControlItemV2>
296
+ </TooltipV2>
297
+ <TooltipV2 openDelay={2000} value={i18n.t("ui.sessionReviewV2.splitDiff")}>
298
+ <SegmentedControlItemV2 value="split" aria-label={i18n.t("ui.sessionReviewV2.splitDiff")}>
299
+ <Icon name="split" />
300
+ </SegmentedControlItemV2>
301
+ </TooltipV2>
302
+ </SegmentedControlV2>
303
+ </Show>
304
+ </>
305
+ )
306
+
307
+ return (
308
+ <SessionFilePanelV2
309
+ sidebar={props.sidebar}
310
+ toolbar={props.hasDiffs}
311
+ toolbarStart={toolbarStart()}
312
+ toolbarEnd={toolbarEnd()}
313
+ >
314
+ <Show when={props.hasDiffs} fallback={props.empty}>
315
+ <Show when={props.activeFile} fallback={<SessionFilePanelV2Empty>{props.empty}</SessionFilePanelV2Empty>}>
316
+ {props.preview}
317
+ </Show>
318
+ </Show>
319
+ </SessionFilePanelV2>
320
+ )
321
+ }
322
+
323
+ export function SessionReviewV2SidebarToggle(props: { opened: boolean; disabled?: boolean; onToggle: () => void }) {
324
+ const i18n = useI18n()
325
+
326
+ return (
327
+ <TooltipV2 value={i18n.t("ui.sessionReviewV2.toggleSidebar")}>
328
+ <IconButtonV2
329
+ variant="ghost"
330
+ size="small"
331
+ class="session-review-v2-sidebar-toggle"
332
+ aria-label={i18n.t("ui.sessionReviewV2.toggleSidebar")}
333
+ aria-expanded={props.opened}
334
+ disabled={props.disabled}
335
+ onClick={props.onToggle}
336
+ icon={<Icon name="filetree" />}
337
+ />
338
+ </TooltipV2>
339
+ )
340
+ }
@@ -0,0 +1,200 @@
1
+ [data-component="tool-error-card"] {
2
+ --tec-border: var(--v2-state-fg-danger);
3
+ --tec-title: var(--v2-text-text-base);
4
+ --tec-sep: var(--v2-text-text-muted);
5
+ --tec-subtitle: var(--v2-text-text-muted);
6
+ --tec-suffix: var(--v2-text-text-faint);
7
+ --tec-chevron: var(--v2-text-text-faint);
8
+ --tec-icon: var(--v2-icon-icon-base);
9
+
10
+ box-sizing: border-box;
11
+ display: flex;
12
+ flex-direction: column;
13
+ align-items: stretch;
14
+ min-width: 0;
15
+ width: 100%;
16
+ padding: 0 0 0 10px;
17
+ gap: 8px;
18
+ border-left: 2px solid var(--tec-border);
19
+ font-variant-numeric: tabular-nums;
20
+
21
+ [data-slot="tool-error-card-trigger"] {
22
+ box-sizing: border-box;
23
+ display: flex;
24
+ flex-direction: row;
25
+ align-items: center;
26
+ gap: 8px;
27
+ width: 100%;
28
+ min-width: 0;
29
+ min-height: 24px;
30
+ padding: 2px 0;
31
+ margin: 0;
32
+ text-align: left;
33
+ color: inherit;
34
+ outline: none;
35
+ border-radius: 2px;
36
+ }
37
+
38
+ [data-slot="tool-error-card-trigger"]:focus-visible {
39
+ outline: 2px solid var(--v2-border-border-focus);
40
+ outline-offset: 2px;
41
+ }
42
+
43
+ [data-slot="tool-error-card-icon-wrap"] {
44
+ display: flex;
45
+ flex-direction: row;
46
+ align-items: center;
47
+ justify-content: center;
48
+ flex-shrink: 0;
49
+ width: 16px;
50
+ height: 20px;
51
+ box-sizing: border-box;
52
+ color: var(--tec-icon);
53
+ }
54
+
55
+ [data-slot="tool-error-card-ban"] {
56
+ display: block;
57
+ flex-shrink: 0;
58
+ }
59
+
60
+ [data-slot="tool-error-card-loader"] {
61
+ display: block;
62
+ flex-shrink: 0;
63
+ width: 16px;
64
+ height: 16px;
65
+ animation: tool-error-card-spin 0.65s linear infinite;
66
+ transform-origin: 50% 50%;
67
+ }
68
+
69
+ [data-slot="tool-error-card-main"] {
70
+ display: flex;
71
+ flex-direction: row;
72
+ align-items: center;
73
+ justify-content: flex-start;
74
+ min-width: 0;
75
+ min-height: 20px;
76
+ flex: 1 1 auto;
77
+ }
78
+
79
+ [data-slot="tool-error-card-labels"] {
80
+ display: inline-flex;
81
+ flex-direction: row;
82
+ align-items: center;
83
+ gap: 6px;
84
+ max-width: 100%;
85
+ min-width: 0;
86
+ overflow: hidden;
87
+ }
88
+
89
+ [data-slot="tool-error-card-title"] {
90
+ display: flex;
91
+ align-items: center;
92
+ flex-shrink: 0;
93
+ max-width: 100%;
94
+ height: 20px;
95
+ font-size: 13px;
96
+ font-weight: 530;
97
+ line-height: 20px;
98
+ letter-spacing: -0.04px;
99
+ color: var(--tec-title);
100
+ user-select: none;
101
+ }
102
+
103
+ [data-slot="tool-error-card-sep"] {
104
+ display: flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ flex-shrink: 0;
108
+ height: 20px;
109
+ font-size: 11px;
110
+ font-weight: 530;
111
+ line-height: 20px;
112
+ letter-spacing: 0.05px;
113
+ color: var(--tec-sep);
114
+ user-select: none;
115
+ }
116
+
117
+ [data-slot="tool-error-card-subtitle"] {
118
+ display: flex;
119
+ align-items: center;
120
+ min-width: 0;
121
+ height: 20px;
122
+ flex: 0 1 auto;
123
+ flex-shrink: 1;
124
+ overflow: hidden;
125
+ text-overflow: ellipsis;
126
+ white-space: nowrap;
127
+ font-size: 13px;
128
+ font-weight: 440;
129
+ line-height: 20px;
130
+ letter-spacing: -0.04px;
131
+ color: var(--tec-subtitle);
132
+ user-select: none;
133
+ }
134
+
135
+ a[data-slot="tool-error-card-subtitle"] {
136
+ display: flex;
137
+ align-items: center;
138
+ min-width: 0;
139
+ height: 20px;
140
+ flex: 0 1 auto;
141
+ flex-shrink: 1;
142
+ overflow: hidden;
143
+ text-overflow: ellipsis;
144
+ white-space: nowrap;
145
+ font-size: 13px;
146
+ font-weight: 440;
147
+ line-height: 20px;
148
+ letter-spacing: -0.04px;
149
+ color: inherit;
150
+ text-decoration: underline;
151
+ text-underline-offset: 2px;
152
+ user-select: none;
153
+ }
154
+
155
+ [data-slot="tool-error-card-chevron-wrap"] {
156
+ display: flex;
157
+ align-items: center;
158
+ justify-content: center;
159
+ flex-shrink: 0;
160
+ width: 14px;
161
+ height: 20px;
162
+ color: var(--tec-chevron);
163
+ user-select: none;
164
+ }
165
+
166
+ [data-slot="tool-error-card-chevron"] {
167
+ display: block;
168
+ flex-shrink: 0;
169
+ width: 14px;
170
+ height: 14px;
171
+ color: var(--tec-chevron);
172
+ transition: transform 0.15s ease-out;
173
+ }
174
+
175
+ &[data-expanded] [data-slot="tool-error-card-chevron"] {
176
+ transform: rotate(90deg);
177
+ }
178
+
179
+ [data-slot="tool-error-card-content"] {
180
+ box-sizing: border-box;
181
+ min-width: 0;
182
+ }
183
+
184
+ [data-slot="tool-error-card-suffix"] {
185
+ padding: 0 0 0 24px;
186
+ font-size: 13px;
187
+ font-weight: 440;
188
+ line-height: 1;
189
+ letter-spacing: -0.04px;
190
+ color: var(--tec-suffix);
191
+ white-space: pre-wrap;
192
+ word-break: break-word;
193
+ }
194
+ }
195
+
196
+ @keyframes tool-error-card-spin {
197
+ to {
198
+ transform: rotate(360deg);
199
+ }
200
+ }
@@ -0,0 +1,91 @@
1
+ import { createSignal } from "solid-js"
2
+ import { ButtonV2 } from "@neurocode-ai/ui/v2/button-v2"
3
+ import { ToolErrorCardV2, type ToolErrorCardV2Props } from "./tool-error-card-v2"
4
+
5
+ const docs = `### Overview
6
+ Compact tool error row with optional expandable detail, aligned to the OpenCode design system spec.
7
+
8
+ ### API
9
+ - \`ToolErrorCardV2\` wraps Kobalte \`Collapsible\` directly. Pass \`open\`, \`defaultOpen\`, and \`onOpenChange\` like any disclosure (controlled when \`open\` is defined).
10
+ - Without a non-empty \`suffix\`, the card is not expandable (\`disabled\` on the collapsible root).
11
+
12
+ ### Theming
13
+ - Uses \`data-component="tool-error-card"\` and slot attributes; colors are CSS variables on the root (\`--tec-*\`).
14
+ `
15
+
16
+ export default {
17
+ title: "UI V2/ToolErrorCard",
18
+ id: "components-tool-error-card-v2",
19
+ component: ToolErrorCardV2,
20
+ tags: ["autodocs"],
21
+ parameters: {
22
+ frameBackground: "#fff",
23
+ layout: "padded",
24
+ docs: {
25
+ description: {
26
+ component: docs,
27
+ },
28
+ },
29
+ },
30
+ }
31
+
32
+ export const Default = {
33
+ args: {
34
+ title: "Read",
35
+ subtitle: "Permission denied",
36
+ suffix: "The tool could not access the requested path.",
37
+ defaultOpen: false,
38
+ } satisfies ToolErrorCardV2Props,
39
+ render: (args: ToolErrorCardV2Props) => <ToolErrorCardV2 {...args} />,
40
+ }
41
+
42
+ export const Loading = {
43
+ args: {
44
+ title: "Read",
45
+ subtitle: "Working",
46
+ suffix: "Details appear when the tool finishes.",
47
+ loading: true,
48
+ defaultOpen: false,
49
+ } satisfies ToolErrorCardV2Props,
50
+ render: (args: ToolErrorCardV2Props) => <ToolErrorCardV2 {...args} />,
51
+ }
52
+
53
+ export const SubtitleLink = {
54
+ args: {
55
+ title: "Task",
56
+ subtitle: "View logs",
57
+ subtitleHref: "https://example.com",
58
+ suffix: "Subagent exited with code 1.",
59
+ defaultOpen: false,
60
+ } satisfies ToolErrorCardV2Props,
61
+ render: (args: ToolErrorCardV2Props) => <ToolErrorCardV2 {...args} />,
62
+ }
63
+
64
+ export const NoSuffixDisabled = {
65
+ args: {
66
+ title: "List",
67
+ subtitle: "No detail",
68
+ defaultOpen: false,
69
+ } satisfies ToolErrorCardV2Props,
70
+ render: (args: ToolErrorCardV2Props) => <ToolErrorCardV2 {...args} />,
71
+ }
72
+
73
+ export const Controlled = {
74
+ render: () => {
75
+ const [open, setOpen] = createSignal(false)
76
+ return (
77
+ <div style={{ display: "flex", "flex-direction": "column", gap: "24px", "max-width": "420px" }}>
78
+ <ButtonV2 type="button" classList={{ "w-fit": true }} onClick={() => setOpen((o) => !o)}>
79
+ Toggle from outside: {open() ? "Open" : "Closed"}
80
+ </ButtonV2>
81
+ <ToolErrorCardV2
82
+ title="Grep"
83
+ subtitle="Timeout"
84
+ suffix="Operation exceeded 30s."
85
+ open={open()}
86
+ onOpenChange={setOpen}
87
+ />
88
+ </div>
89
+ )
90
+ },
91
+ }