@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,343 @@
1
+ import { createEffect, For, Match, on, onCleanup, onMount, Show, Switch, type Accessor, type JSX } from "solid-js"
2
+ import { animate, type AnimationPlaybackControls } from "motion"
3
+ import { useI18n } from "@neurocode-ai/ui/context/i18n"
4
+ import { createStore } from "solid-js/store"
5
+ import { Collapsible } from "@neurocode-ai/ui/collapsible"
6
+ import type { IconProps } from "@neurocode-ai/ui/icon"
7
+ import { TextShimmer } from "@neurocode-ai/ui/text-shimmer"
8
+
9
+ export type TriggerTitle = {
10
+ title: string
11
+ titleClass?: string
12
+ subtitle?: string
13
+ subtitleClass?: string
14
+ args?: string[]
15
+ argsClass?: string
16
+ action?: JSX.Element
17
+ }
18
+
19
+ const isTriggerTitle = (val: any): val is TriggerTitle => {
20
+ return (
21
+ typeof val === "object" && val !== null && "title" in val && (typeof Node === "undefined" || !(val instanceof Node))
22
+ )
23
+ }
24
+
25
+ export interface BasicToolProps {
26
+ icon: IconProps["name"]
27
+ trigger: TriggerTitle | JSX.Element | ((open: Accessor<boolean>) => JSX.Element)
28
+ children?: JSX.Element
29
+ status?: string
30
+ hideDetails?: boolean
31
+ defaultOpen?: boolean
32
+ open?: boolean
33
+ onOpenChange?: (open: boolean) => void
34
+ forceOpen?: boolean
35
+ allowOpenWhilePending?: boolean
36
+ defer?: boolean
37
+ locked?: boolean
38
+ animated?: boolean
39
+ onSubtitleClick?: () => void
40
+ onTriggerClick?: JSX.EventHandlerUnion<HTMLElement, MouseEvent>
41
+ onTriggerKeyDown?: JSX.EventHandlerUnion<HTMLElement, KeyboardEvent>
42
+ triggerHref?: string
43
+ triggerAsLink?: boolean
44
+ clickable?: boolean
45
+ }
46
+
47
+ const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 }
48
+ const deferredMounts: Array<{ active: boolean; fn: () => void }> = []
49
+ let deferredFrame: number | undefined
50
+
51
+ function flushDeferredMounts() {
52
+ while (deferredMounts.length > 0) {
53
+ // Timeline tools are mounted top-to-bottom, but the viewport starts at the latest turn.
54
+ // Pop from the end so heavy default-open bodies near the bottom become interactive first.
55
+ const item = deferredMounts.pop()!
56
+ if (item.active) {
57
+ deferredFrame = deferredMounts.length > 0 ? requestAnimationFrame(flushDeferredMounts) : undefined
58
+ item.fn()
59
+ return
60
+ }
61
+ }
62
+ deferredFrame = undefined
63
+ }
64
+
65
+ function scheduleDeferredFlush() {
66
+ if (deferredFrame !== undefined) return
67
+ deferredFrame = requestAnimationFrame(() => {
68
+ deferredFrame = requestAnimationFrame(flushDeferredMounts)
69
+ })
70
+ }
71
+
72
+ function scheduleDeferredMount(fn: () => void) {
73
+ const item = { active: true, fn }
74
+ deferredMounts.push(item)
75
+ scheduleDeferredFlush()
76
+ return () => {
77
+ item.active = false
78
+ }
79
+ }
80
+
81
+ function scheduleFrameMount(fn: () => void) {
82
+ const frame = requestAnimationFrame(fn)
83
+ return () => cancelAnimationFrame(frame)
84
+ }
85
+
86
+ export function BasicTool(props: BasicToolProps) {
87
+ const [state, setState] = createStore({
88
+ open: props.defaultOpen ?? false,
89
+ ready: !props.defer && (props.defaultOpen ?? false),
90
+ })
91
+ const open = () => props.open ?? state.open
92
+ const ready = () => state.ready
93
+ const pending = () => props.status === "pending" || props.status === "running"
94
+ const hasChildren = () => (props.defer ? "children" in props : props.children)
95
+ const dynamicTrigger = typeof props.trigger === "function" ? props.trigger(open) : undefined
96
+
97
+ let cancelReady: (() => void) | undefined
98
+
99
+ const cancel = () => {
100
+ cancelReady?.()
101
+ cancelReady = undefined
102
+ }
103
+
104
+ const scheduleReady = (initial = false) => {
105
+ cancel()
106
+ cancelReady = (initial ? scheduleDeferredMount : scheduleFrameMount)(() => {
107
+ cancelReady = undefined
108
+ if (!open()) return
109
+ setState("ready", true)
110
+ })
111
+ }
112
+
113
+ onCleanup(cancel)
114
+
115
+ onMount(() => {
116
+ if (props.defer && open()) scheduleReady(true)
117
+ })
118
+
119
+ const setOpen = (value: boolean) => {
120
+ if (props.open === undefined) setState("open", value)
121
+ props.onOpenChange?.(value)
122
+ }
123
+
124
+ createEffect(() => {
125
+ if (!props.forceOpen) return
126
+ if (open()) return
127
+ setOpen(true)
128
+ })
129
+
130
+ createEffect(
131
+ on(
132
+ open,
133
+ (value) => {
134
+ if (!props.defer) return
135
+ if (!value) {
136
+ cancel()
137
+ setState("ready", false)
138
+ return
139
+ }
140
+
141
+ scheduleReady()
142
+ },
143
+ { defer: true },
144
+ ),
145
+ )
146
+
147
+ // Animated height for collapsible open/close
148
+ let contentRef: HTMLDivElement | undefined
149
+ let heightAnim: AnimationPlaybackControls | undefined
150
+ const initialOpen = open()
151
+
152
+ createEffect(
153
+ on(
154
+ open,
155
+ (isOpen) => {
156
+ if (!props.animated || !contentRef) return
157
+ heightAnim?.stop()
158
+ if (isOpen) {
159
+ contentRef.style.overflow = "hidden"
160
+ heightAnim = animate(contentRef, { height: "auto" }, SPRING)
161
+ void heightAnim.finished.then(() => {
162
+ if (!contentRef || !open()) return
163
+ contentRef.style.overflow = "visible"
164
+ contentRef.style.height = "auto"
165
+ })
166
+ } else {
167
+ contentRef.style.overflow = "hidden"
168
+ heightAnim = animate(contentRef, { height: "0px" }, SPRING)
169
+ }
170
+ },
171
+ { defer: true },
172
+ ),
173
+ )
174
+
175
+ onCleanup(() => {
176
+ heightAnim?.stop()
177
+ })
178
+
179
+ const handleOpenChange = (value: boolean) => {
180
+ if (pending() && !props.allowOpenWhilePending) return
181
+ if (props.locked && !value) return
182
+ setOpen(value)
183
+ }
184
+
185
+ const trigger = () => (
186
+ <div
187
+ data-component="tool-trigger"
188
+ data-clickable={props.clickable ? "true" : undefined}
189
+ data-hide-details={props.hideDetails ? "true" : undefined}
190
+ >
191
+ <div data-slot="basic-tool-tool-trigger-content">
192
+ <div data-slot="basic-tool-tool-info">
193
+ <Switch>
194
+ <Match when={dynamicTrigger !== undefined}>{dynamicTrigger}</Match>
195
+ <Match when={isTriggerTitle(props.trigger) && props.trigger}>
196
+ {(title) => (
197
+ <div data-slot="basic-tool-tool-info-structured">
198
+ <div data-slot="basic-tool-tool-info-main">
199
+ <span
200
+ data-slot="basic-tool-tool-title"
201
+ classList={{
202
+ [title().titleClass ?? ""]: !!title().titleClass,
203
+ }}
204
+ >
205
+ <TextShimmer text={title().title} active={pending()} />
206
+ </span>
207
+ <Show when={!pending()}>
208
+ <Show when={title().subtitle}>
209
+ <span
210
+ data-slot="basic-tool-tool-subtitle"
211
+ classList={{
212
+ [title().subtitleClass ?? ""]: !!title().subtitleClass,
213
+ clickable: !!props.onSubtitleClick,
214
+ }}
215
+ onClick={(e) => {
216
+ if (props.onSubtitleClick) {
217
+ e.stopPropagation()
218
+ props.onSubtitleClick()
219
+ }
220
+ }}
221
+ >
222
+ {title().subtitle}
223
+ </span>
224
+ </Show>
225
+ <Show when={title().args?.length}>
226
+ <For each={title().args}>
227
+ {(arg) => (
228
+ <span
229
+ data-slot="basic-tool-tool-arg"
230
+ classList={{
231
+ [title().argsClass ?? ""]: !!title().argsClass,
232
+ }}
233
+ >
234
+ {arg}
235
+ </span>
236
+ )}
237
+ </For>
238
+ </Show>
239
+ </Show>
240
+ </div>
241
+ <Show when={!pending() && title().action}>
242
+ <span data-slot="basic-tool-tool-action">{title().action}</span>
243
+ </Show>
244
+ </div>
245
+ )}
246
+ </Match>
247
+ <Match when={true}>{props.trigger as JSX.Element}</Match>
248
+ </Switch>
249
+ </div>
250
+ </div>
251
+ <Show when={hasChildren() && !props.hideDetails && !props.locked && (!pending() || props.allowOpenWhilePending)}>
252
+ <Collapsible.Arrow />
253
+ </Show>
254
+ </div>
255
+ )
256
+
257
+ return (
258
+ <Collapsible open={open()} onOpenChange={handleOpenChange} class="tool-collapsible">
259
+ <Show
260
+ when={props.triggerAsLink || props.triggerHref}
261
+ fallback={
262
+ <Collapsible.Trigger
263
+ data-hide-details={props.hideDetails ? "true" : undefined}
264
+ onClick={props.onTriggerClick}
265
+ >
266
+ {trigger()}
267
+ </Collapsible.Trigger>
268
+ }
269
+ >
270
+ <Collapsible.Trigger
271
+ as="a"
272
+ href={props.triggerHref}
273
+ role={!props.triggerHref && props.clickable ? "button" : undefined}
274
+ tabIndex={!props.triggerHref && props.clickable ? 0 : undefined}
275
+ data-hide-details={props.hideDetails ? "true" : undefined}
276
+ onClick={props.onTriggerClick}
277
+ onKeyDown={props.onTriggerKeyDown}
278
+ >
279
+ {trigger()}
280
+ </Collapsible.Trigger>
281
+ </Show>
282
+ <Show when={props.animated && hasChildren() && !props.hideDetails}>
283
+ <div
284
+ ref={contentRef}
285
+ data-slot="collapsible-content"
286
+ data-animated
287
+ style={{
288
+ height: initialOpen ? "auto" : "0px",
289
+ overflow: initialOpen ? "visible" : "hidden",
290
+ }}
291
+ >
292
+ <Show when={!props.defer || ready()}>{props.children}</Show>
293
+ </div>
294
+ </Show>
295
+ <Show when={!props.animated && hasChildren() && !props.hideDetails}>
296
+ <Collapsible.Content>
297
+ <Show when={!props.defer || ready()}>{props.children}</Show>
298
+ </Collapsible.Content>
299
+ </Show>
300
+ </Collapsible>
301
+ )
302
+ }
303
+
304
+ function label(input: Record<string, unknown> | undefined) {
305
+ const keys = ["description", "query", "url", "filePath", "path", "pattern", "name"]
306
+ return keys.map((key) => input?.[key]).find((value): value is string => typeof value === "string" && value.length > 0)
307
+ }
308
+
309
+ function args(input: Record<string, unknown> | undefined) {
310
+ if (!input) return []
311
+ const skip = new Set(["description", "query", "url", "filePath", "path", "pattern", "name"])
312
+ return Object.entries(input)
313
+ .filter(([key]) => !skip.has(key))
314
+ .flatMap(([key, value]) => {
315
+ if (typeof value === "string") return [`${key}=${value}`]
316
+ if (typeof value === "number") return [`${key}=${value}`]
317
+ if (typeof value === "boolean") return [`${key}=${value}`]
318
+ return []
319
+ })
320
+ .slice(0, 3)
321
+ }
322
+
323
+ export function GenericTool(props: {
324
+ tool: string
325
+ status?: string
326
+ hideDetails?: boolean
327
+ input?: Record<string, unknown>
328
+ }) {
329
+ const i18n = useI18n()
330
+
331
+ return (
332
+ <BasicTool
333
+ icon="mcp"
334
+ status={props.status}
335
+ trigger={{
336
+ title: i18n.t("ui.basicTool.called", { tool: props.tool }),
337
+ subtitle: label(props.input),
338
+ args: args(props.input),
339
+ }}
340
+ hideDetails={props.hideDetails}
341
+ />
342
+ )
343
+ }
@@ -0,0 +1,62 @@
1
+ // @ts-nocheck
2
+ import * as mod from "./dock-prompt"
3
+ import { create } from "@neurocode-ai/ui/storybook/scaffold"
4
+
5
+ const docs = `### Overview
6
+ Docked prompt layout for questions and permission requests.
7
+
8
+ Use with form controls or confirmation buttons in the footer.
9
+
10
+ ### API
11
+ - Required: \`kind\` (question | permission), \`header\`, \`children\`, \`footer\`.
12
+ - Optional: \`ref\` for measuring or focus management.
13
+
14
+ ### Variants and states
15
+ - Question and permission layouts (data attributes).
16
+
17
+ ### Behavior
18
+ - Pure layout component; behavior handled by parent.
19
+
20
+ ### Accessibility
21
+ - Ensure header and footer content provide clear context and actions.
22
+
23
+ ### Theming/tokens
24
+ - Uses \`data-component="dock-prompt"\` with kind data attribute.
25
+
26
+ `
27
+
28
+ const story = create({
29
+ title: "UI/DockPrompt",
30
+ mod,
31
+ args: {
32
+ kind: "question",
33
+ header: "Header",
34
+ children: "Prompt content",
35
+ footer: "Footer",
36
+ },
37
+ })
38
+
39
+ export default {
40
+ title: "UI/DockPrompt",
41
+ id: "components-dock-prompt",
42
+ component: story.meta.component,
43
+ tags: ["autodocs"],
44
+ parameters: {
45
+ docs: {
46
+ description: {
47
+ component: docs,
48
+ },
49
+ },
50
+ },
51
+ }
52
+
53
+ export const Basic = story.Basic
54
+
55
+ export const Permission = {
56
+ args: {
57
+ kind: "permission",
58
+ header: "Allow access?",
59
+ children: "This action needs permission to proceed.",
60
+ footer: "Approve or deny",
61
+ },
62
+ }
@@ -0,0 +1,23 @@
1
+ import type { JSX } from "solid-js"
2
+ import { DockShell, DockTray } from "@neurocode-ai/ui/dock-surface"
3
+
4
+ export function DockPrompt(props: {
5
+ kind: "question" | "permission"
6
+ header: JSX.Element
7
+ children: JSX.Element
8
+ footer: JSX.Element
9
+ ref?: (el: HTMLDivElement) => void
10
+ onKeyDown?: JSX.EventHandlerUnion<HTMLDivElement, KeyboardEvent>
11
+ }) {
12
+ const slot = (name: string) => `${props.kind}-${name}`
13
+
14
+ return (
15
+ <div data-component="dock-prompt" data-kind={props.kind} ref={props.ref} onKeyDown={props.onKeyDown}>
16
+ <DockShell data-slot={slot("body")}>
17
+ <div data-slot={slot("header")}>{props.header}</div>
18
+ <div data-slot={slot("content")}>{props.children}</div>
19
+ </DockShell>
20
+ <DockTray data-slot={slot("footer")}>{props.footer}</DockTray>
21
+ </div>
22
+ )
23
+ }