@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.
- package/package.json +71 -0
- package/src/components/apply-patch-file.test.ts +43 -0
- package/src/components/apply-patch-file.ts +78 -0
- package/src/components/basic-tool.css +358 -0
- package/src/components/basic-tool.stories.tsx +133 -0
- package/src/components/basic-tool.tsx +343 -0
- package/src/components/dock-prompt.stories.tsx +62 -0
- package/src/components/dock-prompt.tsx +23 -0
- package/src/components/file-media.tsx +291 -0
- package/src/components/file-search.tsx +72 -0
- package/src/components/file-ssr.tsx +197 -0
- package/src/components/file.css +46 -0
- package/src/components/file.tsx +1202 -0
- package/src/components/line-comment-annotations.tsx +637 -0
- package/src/components/line-comment-styles.ts +292 -0
- package/src/components/line-comment.stories.tsx +115 -0
- package/src/components/line-comment.tsx +439 -0
- package/src/components/markdown-cache.tsx +78 -0
- package/src/components/markdown-code-state.test.ts +32 -0
- package/src/components/markdown-code-state.ts +22 -0
- package/src/components/markdown-inline-code-kind.test.ts +46 -0
- package/src/components/markdown-inline-code-kind.ts +1915 -0
- package/src/components/markdown-preload.test.ts +18 -0
- package/src/components/markdown-shiki.worker.ts +104 -0
- package/src/components/markdown-stream.test.ts +194 -0
- package/src/components/markdown-stream.ts +110 -0
- package/src/components/markdown-worker-protocol.test.ts +81 -0
- package/src/components/markdown-worker-protocol.ts +48 -0
- package/src/components/markdown-worker-queue.test.ts +49 -0
- package/src/components/markdown-worker-queue.ts +64 -0
- package/src/components/markdown-worker-transport.test.ts +56 -0
- package/src/components/markdown-worker-transport.ts +41 -0
- package/src/components/markdown-worker.ts +122 -0
- package/src/components/markdown.css +377 -0
- package/src/components/markdown.stories.tsx +53 -0
- package/src/components/markdown.tsx +678 -0
- package/src/components/message-file.test.ts +63 -0
- package/src/components/message-file.ts +34 -0
- package/src/components/message-nav.css +127 -0
- package/src/components/message-nav.stories.tsx +7 -0
- package/src/components/message-nav.tsx +102 -0
- package/src/components/message-part-text.ts +3 -0
- package/src/components/message-part.css +1569 -0
- package/src/components/message-part.stories.tsx +7 -0
- package/src/components/message-part.test.ts +28 -0
- package/src/components/message-part.tsx +2662 -0
- package/src/components/session-diff.test.ts +135 -0
- package/src/components/session-diff.ts +145 -0
- package/src/components/session-retry.tsx +74 -0
- package/src/components/session-review.css +247 -0
- package/src/components/session-review.stories.tsx +7 -0
- package/src/components/session-review.tsx +657 -0
- package/src/components/session-turn.css +241 -0
- package/src/components/session-turn.stories.tsx +7 -0
- package/src/components/session-turn.tsx +543 -0
- package/src/components/shell-submessage-motion.stories.tsx +346 -0
- package/src/components/shell-submessage.css +23 -0
- package/src/components/timeline-playground.stories.tsx +2090 -0
- package/src/components/tool-count-label.css +57 -0
- package/src/components/tool-count-label.tsx +58 -0
- package/src/components/tool-count-summary.css +102 -0
- package/src/components/tool-count-summary.stories.tsx +238 -0
- package/src/components/tool-count-summary.tsx +52 -0
- package/src/components/tool-error-card.css +91 -0
- package/src/components/tool-error-card.stories.tsx +92 -0
- package/src/components/tool-error-card.tsx +157 -0
- package/src/components/tool-status-title.css +89 -0
- package/src/components/tool-status-title.tsx +133 -0
- package/src/context/data.tsx +64 -0
- package/src/context/index.ts +1 -0
- package/src/pierre/comment-hover.ts +83 -0
- package/src/pierre/commented-lines.ts +91 -0
- package/src/pierre/diff-selection.ts +71 -0
- package/src/pierre/file-find.ts +485 -0
- package/src/pierre/file-runtime.ts +114 -0
- package/src/pierre/file-selection.ts +85 -0
- package/src/pierre/index.ts +186 -0
- package/src/pierre/media.ts +110 -0
- package/src/pierre/selection-bridge.ts +132 -0
- package/src/pierre/virtualizer.ts +100 -0
- package/src/pierre/worker.ts +52 -0
- package/src/styles/index.css +14 -0
- package/src/v2/components/attachment-card-v2.css +70 -0
- package/src/v2/components/attachment-card-v2.tsx +33 -0
- package/src/v2/components/basic-tool-v2.css +163 -0
- package/src/v2/components/basic-tool-v2.stories.tsx +137 -0
- package/src/v2/components/basic-tool-v2.tsx +139 -0
- package/src/v2/components/comment-card-v2.tsx +64 -0
- package/src/v2/components/line-comment-annotations-v2.tsx +220 -0
- package/src/v2/components/prompt-input/attachments.ts +266 -0
- package/src/v2/components/prompt-input/index.tsx +706 -0
- package/src/v2/components/prompt-input/interaction.ts +482 -0
- package/src/v2/components/prompt-input/machine.test.ts +164 -0
- package/src/v2/components/prompt-input/machine.ts +261 -0
- package/src/v2/components/prompt-input/prompt-input.stories.tsx +221 -0
- package/src/v2/components/prompt-input/store.test.ts +116 -0
- package/src/v2/components/prompt-input/store.ts +152 -0
- package/src/v2/components/prompt-input/types.ts +106 -0
- package/src/v2/components/session-file-panel-v2.tsx +43 -0
- package/src/v2/components/session-progress-indicator-v2.css +875 -0
- package/src/v2/components/session-progress-indicator-v2.stories.tsx +66 -0
- package/src/v2/components/session-progress-indicator-v2.tsx +32 -0
- package/src/v2/components/session-review-empty-changes-v2.tsx +17 -0
- package/src/v2/components/session-review-empty-no-git-v2.tsx +28 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.test.ts +13 -0
- package/src/v2/components/session-review-file-preview-v2-virtualize.ts +5 -0
- package/src/v2/components/session-review-file-preview-v2.tsx +290 -0
- package/src/v2/components/session-review-v2.css +458 -0
- package/src/v2/components/session-review-v2.tsx +340 -0
- package/src/v2/components/tool-error-card-v2.css +200 -0
- package/src/v2/components/tool-error-card-v2.stories.tsx +91 -0
- package/src/v2/components/tool-error-card-v2.tsx +166 -0
- package/sst-env.d.ts +10 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { type ComponentProps, createMemo, Show, splitProps } from "solid-js"
|
|
2
|
+
import { createStore } from "solid-js/store"
|
|
3
|
+
import { Card, CardDescription } from "@neurocode-ai/ui/card"
|
|
4
|
+
import { Collapsible } from "@neurocode-ai/ui/collapsible"
|
|
5
|
+
import { Icon } from "@neurocode-ai/ui/icon"
|
|
6
|
+
import { IconButton } from "@neurocode-ai/ui/icon-button"
|
|
7
|
+
import { Tooltip } from "@neurocode-ai/ui/tooltip"
|
|
8
|
+
import { useI18n } from "@neurocode-ai/ui/context/i18n"
|
|
9
|
+
|
|
10
|
+
export interface ToolErrorCardProps extends Omit<ComponentProps<typeof Card>, "children" | "variant"> {
|
|
11
|
+
tool: string
|
|
12
|
+
error: string
|
|
13
|
+
title?: string
|
|
14
|
+
defaultOpen?: boolean
|
|
15
|
+
open?: boolean
|
|
16
|
+
onOpenChange?: (open: boolean) => void
|
|
17
|
+
subtitle?: string
|
|
18
|
+
href?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ToolErrorCard(props: ToolErrorCardProps) {
|
|
22
|
+
const i18n = useI18n()
|
|
23
|
+
const [state, setState] = createStore({
|
|
24
|
+
open: props.defaultOpen ?? false,
|
|
25
|
+
copied: false,
|
|
26
|
+
})
|
|
27
|
+
const open = () => props.open ?? state.open
|
|
28
|
+
const copied = () => state.copied
|
|
29
|
+
const [split, rest] = splitProps(props, [
|
|
30
|
+
"tool",
|
|
31
|
+
"error",
|
|
32
|
+
"title",
|
|
33
|
+
"defaultOpen",
|
|
34
|
+
"open",
|
|
35
|
+
"onOpenChange",
|
|
36
|
+
"subtitle",
|
|
37
|
+
"href",
|
|
38
|
+
])
|
|
39
|
+
const setOpen = (value: boolean) => {
|
|
40
|
+
if (props.open === undefined) setState("open", value)
|
|
41
|
+
props.onOpenChange?.(value)
|
|
42
|
+
}
|
|
43
|
+
const name = createMemo(() => {
|
|
44
|
+
if (split.title) return split.title
|
|
45
|
+
const map: Record<string, string> = {
|
|
46
|
+
read: "ui.tool.read",
|
|
47
|
+
list: "ui.tool.list",
|
|
48
|
+
glob: "ui.tool.glob",
|
|
49
|
+
grep: "ui.tool.grep",
|
|
50
|
+
task: "ui.tool.task",
|
|
51
|
+
webfetch: "ui.tool.webfetch",
|
|
52
|
+
websearch: "ui.tool.websearch",
|
|
53
|
+
bash: "ui.tool.shell",
|
|
54
|
+
shell: "ui.tool.shell",
|
|
55
|
+
patch: "ui.tool.patch",
|
|
56
|
+
apply_patch: "ui.tool.patch",
|
|
57
|
+
question: "ui.tool.questions",
|
|
58
|
+
}
|
|
59
|
+
const key = map[split.tool]
|
|
60
|
+
if (!key) return split.tool
|
|
61
|
+
if (!key.includes(".")) return key
|
|
62
|
+
return i18n.t(key)
|
|
63
|
+
})
|
|
64
|
+
const cleaned = createMemo(() => split.error.replace(/^Error:\s*/, "").trim())
|
|
65
|
+
const tail = createMemo(() => {
|
|
66
|
+
const value = cleaned()
|
|
67
|
+
const prefix = `${split.tool} `
|
|
68
|
+
if (value.startsWith(prefix)) return value.slice(prefix.length)
|
|
69
|
+
return value
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const subtitle = createMemo(() => {
|
|
73
|
+
if (split.subtitle) return split.subtitle
|
|
74
|
+
const parts = tail().split(": ")
|
|
75
|
+
if (parts.length <= 1) return i18n.t("ui.toolErrorCard.failed")
|
|
76
|
+
const head = (parts[0] ?? "").trim()
|
|
77
|
+
if (!head) return i18n.t("ui.toolErrorCard.failed")
|
|
78
|
+
return head[0] ? head[0].toUpperCase() + head.slice(1) : i18n.t("ui.toolErrorCard.failed")
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
const body = createMemo(() => {
|
|
82
|
+
const parts = tail().split(": ")
|
|
83
|
+
if (parts.length <= 1) return cleaned()
|
|
84
|
+
return parts.slice(1).join(": ").trim() || cleaned()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const copy = async () => {
|
|
88
|
+
const text = cleaned()
|
|
89
|
+
if (!text) return
|
|
90
|
+
await navigator.clipboard.writeText(text)
|
|
91
|
+
setState("copied", true)
|
|
92
|
+
setTimeout(() => setState("copied", false), 2000)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Card {...rest} data-kind="tool-error-card" data-open={open() ? "true" : "false"} variant="error">
|
|
97
|
+
<Collapsible class="tool-collapsible" data-open={open() ? "true" : "false"} open={open()} onOpenChange={setOpen}>
|
|
98
|
+
<Collapsible.Trigger>
|
|
99
|
+
<div data-component="tool-trigger">
|
|
100
|
+
<div data-slot="basic-tool-tool-trigger-content">
|
|
101
|
+
<span data-slot="basic-tool-tool-indicator" data-component="tool-error-card-icon">
|
|
102
|
+
<Icon name="circle-ban-sign" size="small" style={{ "stroke-width": 1.5 }} />
|
|
103
|
+
</span>
|
|
104
|
+
<div data-slot="basic-tool-tool-info">
|
|
105
|
+
<div data-slot="basic-tool-tool-info-structured">
|
|
106
|
+
<div data-slot="basic-tool-tool-info-main">
|
|
107
|
+
<span data-slot="basic-tool-tool-title">{name()}</span>
|
|
108
|
+
<Show
|
|
109
|
+
when={split.href && split.subtitle}
|
|
110
|
+
fallback={<span data-slot="basic-tool-tool-subtitle">{subtitle()}</span>}
|
|
111
|
+
>
|
|
112
|
+
<a
|
|
113
|
+
data-slot="basic-tool-tool-subtitle"
|
|
114
|
+
class="clickable subagent-link"
|
|
115
|
+
href={split.href!}
|
|
116
|
+
onClick={(e) => e.stopPropagation()}
|
|
117
|
+
>
|
|
118
|
+
{subtitle()}
|
|
119
|
+
</a>
|
|
120
|
+
</Show>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
<Collapsible.Arrow />
|
|
126
|
+
</div>
|
|
127
|
+
</Collapsible.Trigger>
|
|
128
|
+
<Collapsible.Content>
|
|
129
|
+
<div data-slot="tool-error-card-content">
|
|
130
|
+
<Show when={open()}>
|
|
131
|
+
<div data-slot="tool-error-card-copy">
|
|
132
|
+
<Tooltip
|
|
133
|
+
value={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")}
|
|
134
|
+
placement="top"
|
|
135
|
+
gutter={4}
|
|
136
|
+
>
|
|
137
|
+
<IconButton
|
|
138
|
+
icon={copied() ? "check" : "copy"}
|
|
139
|
+
size="normal"
|
|
140
|
+
variant="ghost"
|
|
141
|
+
onMouseDown={(e) => e.preventDefault()}
|
|
142
|
+
onClick={(e) => {
|
|
143
|
+
e.stopPropagation()
|
|
144
|
+
void copy()
|
|
145
|
+
}}
|
|
146
|
+
aria-label={copied() ? i18n.t("ui.message.copied") : i18n.t("ui.toolErrorCard.copyError")}
|
|
147
|
+
/>
|
|
148
|
+
</Tooltip>
|
|
149
|
+
</div>
|
|
150
|
+
</Show>
|
|
151
|
+
<Show when={body()}>{(value) => <CardDescription>{value()}</CardDescription>}</Show>
|
|
152
|
+
</div>
|
|
153
|
+
</Collapsible.Content>
|
|
154
|
+
</Collapsible>
|
|
155
|
+
</Card>
|
|
156
|
+
)
|
|
157
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
[data-component="tool-status-title"] {
|
|
2
|
+
display: inline-flex;
|
|
3
|
+
align-items: baseline;
|
|
4
|
+
white-space: nowrap;
|
|
5
|
+
text-align: start;
|
|
6
|
+
|
|
7
|
+
[data-slot="tool-status-suffix"] {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: baseline;
|
|
10
|
+
white-space: nowrap;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
[data-slot="tool-status-prefix"] {
|
|
14
|
+
white-space: nowrap;
|
|
15
|
+
flex-shrink: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
[data-slot="tool-status-swap"],
|
|
19
|
+
[data-slot="tool-status-tail"] {
|
|
20
|
+
display: inline-grid;
|
|
21
|
+
overflow: hidden;
|
|
22
|
+
justify-items: start;
|
|
23
|
+
transition: width var(--tool-motion-spring-ms, 480ms) var(--tool-motion-ease, cubic-bezier(0.22, 1, 0.36, 1));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
[data-slot="tool-status-active"],
|
|
27
|
+
[data-slot="tool-status-done"] {
|
|
28
|
+
grid-area: 1 / 1;
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
justify-self: start;
|
|
31
|
+
text-align: start;
|
|
32
|
+
transition-property: opacity, filter, transform;
|
|
33
|
+
transition-duration:
|
|
34
|
+
var(--tool-motion-fade-ms, 240ms), calc(var(--tool-motion-fade-ms, 240ms) * 0.8),
|
|
35
|
+
calc(var(--tool-motion-fade-ms, 240ms) * 0.8);
|
|
36
|
+
transition-timing-function: ease-out, ease-out, ease-out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
&[data-ready="false"] {
|
|
40
|
+
[data-slot="tool-status-swap"],
|
|
41
|
+
[data-slot="tool-status-tail"] {
|
|
42
|
+
transition-duration: 0ms;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[data-slot="tool-status-active"],
|
|
46
|
+
[data-slot="tool-status-done"] {
|
|
47
|
+
transition-duration: 0ms;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
[data-slot="tool-status-active"] {
|
|
52
|
+
opacity: 0;
|
|
53
|
+
filter: blur(calc(var(--tool-motion-blur, 2px) * 0.45));
|
|
54
|
+
transform: translateY(0.03em);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
[data-slot="tool-status-done"] {
|
|
58
|
+
color: var(--text-strong);
|
|
59
|
+
opacity: 1;
|
|
60
|
+
filter: blur(0);
|
|
61
|
+
transform: translateY(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&[data-active="true"] {
|
|
65
|
+
[data-slot="tool-status-active"] {
|
|
66
|
+
opacity: 1;
|
|
67
|
+
filter: blur(0);
|
|
68
|
+
transform: translateY(0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
[data-slot="tool-status-done"] {
|
|
72
|
+
opacity: 0;
|
|
73
|
+
filter: blur(calc(var(--tool-motion-blur, 2px) * 0.45));
|
|
74
|
+
transform: translateY(0.03em);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@media (prefers-reduced-motion: reduce) {
|
|
80
|
+
[data-component="tool-status-title"] [data-slot="tool-status-swap"],
|
|
81
|
+
[data-component="tool-status-title"] [data-slot="tool-status-tail"] {
|
|
82
|
+
transition-duration: 0ms;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
[data-component="tool-status-title"] [data-slot="tool-status-active"],
|
|
86
|
+
[data-component="tool-status-title"] [data-slot="tool-status-done"] {
|
|
87
|
+
transition-duration: 0ms;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Show, createEffect, createMemo, on, onCleanup } from "solid-js"
|
|
2
|
+
import { createStore } from "solid-js/store"
|
|
3
|
+
import { TextShimmer } from "@neurocode-ai/ui/text-shimmer"
|
|
4
|
+
|
|
5
|
+
function common(active: string, done: string) {
|
|
6
|
+
const a = Array.from(active)
|
|
7
|
+
const b = Array.from(done)
|
|
8
|
+
let i = 0
|
|
9
|
+
while (i < a.length && i < b.length && a[i] === b[i]) i++
|
|
10
|
+
return {
|
|
11
|
+
prefix: a.slice(0, i).join(""),
|
|
12
|
+
active: a.slice(i).join(""),
|
|
13
|
+
done: b.slice(i).join(""),
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function contentWidth(el: HTMLSpanElement | undefined) {
|
|
18
|
+
if (!el) return
|
|
19
|
+
return `${Math.ceil(el.getBoundingClientRect().width)}px`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ToolStatusTitle(props: {
|
|
23
|
+
active: boolean
|
|
24
|
+
activeText: string
|
|
25
|
+
doneText: string
|
|
26
|
+
class?: string
|
|
27
|
+
split?: boolean
|
|
28
|
+
}) {
|
|
29
|
+
const split = createMemo(() => common(props.activeText, props.doneText))
|
|
30
|
+
const suffix = createMemo(
|
|
31
|
+
() => (props.split ?? true) && split().prefix.length >= 2 && split().active.length > 0 && split().done.length > 0,
|
|
32
|
+
)
|
|
33
|
+
const prefixLen = createMemo(() => Array.from(split().prefix).length)
|
|
34
|
+
const activeTail = createMemo(() => (suffix() ? split().active : props.activeText))
|
|
35
|
+
const doneTail = createMemo(() => (suffix() ? split().done : props.doneText))
|
|
36
|
+
|
|
37
|
+
const [state, setState] = createStore({
|
|
38
|
+
active: props.active,
|
|
39
|
+
animating: false,
|
|
40
|
+
width: undefined as string | undefined,
|
|
41
|
+
})
|
|
42
|
+
const width = () => state.width
|
|
43
|
+
const active = () => state.active
|
|
44
|
+
const animating = () => state.animating
|
|
45
|
+
let activeRef: HTMLSpanElement | undefined
|
|
46
|
+
let doneRef: HTMLSpanElement | undefined
|
|
47
|
+
let widthRef: HTMLSpanElement | undefined
|
|
48
|
+
let frame: number | undefined
|
|
49
|
+
let finishTimer: ReturnType<typeof setTimeout> | undefined
|
|
50
|
+
|
|
51
|
+
const finish = () => {
|
|
52
|
+
if (frame !== undefined) cancelAnimationFrame(frame)
|
|
53
|
+
if (finishTimer !== undefined) clearTimeout(finishTimer)
|
|
54
|
+
frame = undefined
|
|
55
|
+
finishTimer = undefined
|
|
56
|
+
setState("animating", false)
|
|
57
|
+
setState("width", undefined)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const animate = () => {
|
|
61
|
+
const first = contentWidth(widthRef)
|
|
62
|
+
const next = props.active
|
|
63
|
+
finish()
|
|
64
|
+
setState("active", next)
|
|
65
|
+
if (!first) return
|
|
66
|
+
|
|
67
|
+
setState("animating", true)
|
|
68
|
+
setState("width", first)
|
|
69
|
+
frame = requestAnimationFrame(() => {
|
|
70
|
+
frame = undefined
|
|
71
|
+
const last = contentWidth(next ? activeRef : doneRef)
|
|
72
|
+
if (!last) {
|
|
73
|
+
finish()
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (first !== last) setState("width", last)
|
|
77
|
+
finishTimer = setTimeout(finish, 600)
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
createEffect(on([() => props.active, activeTail, doneTail], () => animate(), { defer: true }))
|
|
82
|
+
|
|
83
|
+
onCleanup(() => {
|
|
84
|
+
finish()
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return (
|
|
88
|
+
<span
|
|
89
|
+
data-component="tool-status-title"
|
|
90
|
+
data-active={active() ? "true" : "false"}
|
|
91
|
+
data-ready={animating() ? "true" : "false"}
|
|
92
|
+
data-mode={suffix() ? "suffix" : "swap"}
|
|
93
|
+
class={props.class}
|
|
94
|
+
aria-label={active() ? props.activeText : props.doneText}
|
|
95
|
+
>
|
|
96
|
+
<Show
|
|
97
|
+
when={suffix()}
|
|
98
|
+
fallback={
|
|
99
|
+
<span data-slot="tool-status-swap" ref={widthRef} style={{ width: width() }}>
|
|
100
|
+
<Show when={animating() || active()}>
|
|
101
|
+
<span data-slot="tool-status-active" ref={activeRef}>
|
|
102
|
+
<TextShimmer text={activeTail()} active={active()} offset={0} />
|
|
103
|
+
</span>
|
|
104
|
+
</Show>
|
|
105
|
+
<Show when={animating() || !active()}>
|
|
106
|
+
<span data-slot="tool-status-done" ref={doneRef}>
|
|
107
|
+
<TextShimmer text={doneTail()} active={false} offset={0} />
|
|
108
|
+
</span>
|
|
109
|
+
</Show>
|
|
110
|
+
</span>
|
|
111
|
+
}
|
|
112
|
+
>
|
|
113
|
+
<span data-slot="tool-status-suffix">
|
|
114
|
+
<span data-slot="tool-status-prefix">
|
|
115
|
+
<TextShimmer text={split().prefix} active={active()} offset={0} />
|
|
116
|
+
</span>
|
|
117
|
+
<span data-slot="tool-status-tail" ref={widthRef} style={{ width: width() }}>
|
|
118
|
+
<Show when={animating() || active()}>
|
|
119
|
+
<span data-slot="tool-status-active" ref={activeRef}>
|
|
120
|
+
<TextShimmer text={activeTail()} active={active()} offset={prefixLen()} />
|
|
121
|
+
</span>
|
|
122
|
+
</Show>
|
|
123
|
+
<Show when={animating() || !active()}>
|
|
124
|
+
<span data-slot="tool-status-done" ref={doneRef}>
|
|
125
|
+
<TextShimmer text={doneTail()} active={false} offset={prefixLen()} />
|
|
126
|
+
</span>
|
|
127
|
+
</Show>
|
|
128
|
+
</span>
|
|
129
|
+
</span>
|
|
130
|
+
</Show>
|
|
131
|
+
</span>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Message, Session, Part, SnapshotFileDiff, SessionStatus, Provider } from "@neurocode-ai/sdk/v2"
|
|
2
|
+
import type { FileDiffInfo } from "@neurocode-ai/client/promise"
|
|
3
|
+
import { createSimpleContext } from "@neurocode-ai/ui/context"
|
|
4
|
+
import { PreloadMultiFileDiffResult } from "@pierre/diffs/ssr"
|
|
5
|
+
|
|
6
|
+
export type NormalizedProviderListResponse = {
|
|
7
|
+
all: Map<string, Provider>
|
|
8
|
+
default: {
|
|
9
|
+
[key: string]: string
|
|
10
|
+
}
|
|
11
|
+
connected: Array<string>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type Data = {
|
|
15
|
+
agent?: {
|
|
16
|
+
name: string
|
|
17
|
+
color?: string
|
|
18
|
+
}[]
|
|
19
|
+
provider?: NormalizedProviderListResponse
|
|
20
|
+
session: Session[]
|
|
21
|
+
session_status: {
|
|
22
|
+
[sessionID: string]: SessionStatus
|
|
23
|
+
}
|
|
24
|
+
session_diff: {
|
|
25
|
+
[sessionID: string]: (SnapshotFileDiff | FileDiffInfo)[]
|
|
26
|
+
}
|
|
27
|
+
session_diff_preload?: {
|
|
28
|
+
[sessionID: string]: PreloadMultiFileDiffResult<any>[]
|
|
29
|
+
}
|
|
30
|
+
message: {
|
|
31
|
+
[sessionID: string]: Message[]
|
|
32
|
+
}
|
|
33
|
+
part: {
|
|
34
|
+
[messageID: string]: Part[]
|
|
35
|
+
}
|
|
36
|
+
part_text_accum_delta?: {
|
|
37
|
+
[partID: string]: string
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type NavigateToSessionFn = (sessionID: string) => void
|
|
42
|
+
|
|
43
|
+
export type SessionHrefFn = (sessionID: string) => string
|
|
44
|
+
|
|
45
|
+
export const { use: useData, provider: DataProvider } = createSimpleContext({
|
|
46
|
+
name: "Data",
|
|
47
|
+
init: (props: {
|
|
48
|
+
data: Data
|
|
49
|
+
directory: string
|
|
50
|
+
onNavigateToSession?: NavigateToSessionFn
|
|
51
|
+
onSessionHref?: SessionHrefFn
|
|
52
|
+
}) => {
|
|
53
|
+
return {
|
|
54
|
+
get store() {
|
|
55
|
+
return props.data
|
|
56
|
+
},
|
|
57
|
+
get directory() {
|
|
58
|
+
return props.directory
|
|
59
|
+
},
|
|
60
|
+
navigateToSession: props.onNavigateToSession,
|
|
61
|
+
sessionHref: props.onSessionHref,
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
})
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./data"
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type HoverCommentLine = {
|
|
2
|
+
lineNumber: number
|
|
3
|
+
side?: "additions" | "deletions"
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function createHoverCommentUtility(props: {
|
|
7
|
+
label: string
|
|
8
|
+
getHoveredLine: () => HoverCommentLine | undefined
|
|
9
|
+
onSelect: (line: HoverCommentLine) => void
|
|
10
|
+
}) {
|
|
11
|
+
if (typeof document === "undefined") return
|
|
12
|
+
|
|
13
|
+
const button = document.createElement("button")
|
|
14
|
+
button.type = "button"
|
|
15
|
+
button.ariaLabel = props.label
|
|
16
|
+
button.textContent = "+"
|
|
17
|
+
button.style.width = "20px"
|
|
18
|
+
button.style.height = "20px"
|
|
19
|
+
button.style.display = "flex"
|
|
20
|
+
button.style.alignItems = "center"
|
|
21
|
+
button.style.justifyContent = "center"
|
|
22
|
+
button.style.border = "none"
|
|
23
|
+
button.style.borderRadius = "var(--radius-md)"
|
|
24
|
+
button.style.background = "var(--icon-interactive-base)"
|
|
25
|
+
button.style.color = "var(--white)"
|
|
26
|
+
button.style.boxShadow = "var(--shadow-xs)"
|
|
27
|
+
button.style.fontSize = "14px"
|
|
28
|
+
button.style.lineHeight = "1"
|
|
29
|
+
button.style.cursor = "pointer"
|
|
30
|
+
button.style.position = "relative"
|
|
31
|
+
button.style.left = "30px"
|
|
32
|
+
button.style.top = "calc((var(--diffs-line-height, 24px) - 20px) / 2)"
|
|
33
|
+
|
|
34
|
+
let line: HoverCommentLine | undefined
|
|
35
|
+
|
|
36
|
+
const sync = () => {
|
|
37
|
+
const next = props.getHoveredLine()
|
|
38
|
+
if (!next) return
|
|
39
|
+
line = next
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The hovered line changes when the pointer moves or when content scrolls under
|
|
43
|
+
// a stationary pointer, so track both with passive listeners instead of polling
|
|
44
|
+
// every animation frame. There is no teardown hook for this utility; like the
|
|
45
|
+
// rAF loop this replaced, the listeners self-detach on the first event after
|
|
46
|
+
// the button leaves the DOM.
|
|
47
|
+
const onHoverInvalidated = () => {
|
|
48
|
+
if (!button.isConnected) {
|
|
49
|
+
document.removeEventListener("pointermove", onHoverInvalidated)
|
|
50
|
+
document.removeEventListener("scroll", onHoverInvalidated, true)
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
sync()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const open = () => {
|
|
57
|
+
const next = props.getHoveredLine() ?? line
|
|
58
|
+
if (!next) return
|
|
59
|
+
props.onSelect(next)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
document.addEventListener("pointermove", onHoverInvalidated, { passive: true })
|
|
63
|
+
document.addEventListener("scroll", onHoverInvalidated, { passive: true, capture: true })
|
|
64
|
+
button.addEventListener("mouseenter", sync)
|
|
65
|
+
button.addEventListener("mousemove", sync)
|
|
66
|
+
button.addEventListener("pointerdown", (event) => {
|
|
67
|
+
event.preventDefault()
|
|
68
|
+
event.stopPropagation()
|
|
69
|
+
sync()
|
|
70
|
+
})
|
|
71
|
+
button.addEventListener("mousedown", (event) => {
|
|
72
|
+
event.preventDefault()
|
|
73
|
+
event.stopPropagation()
|
|
74
|
+
sync()
|
|
75
|
+
})
|
|
76
|
+
button.addEventListener("click", (event) => {
|
|
77
|
+
event.preventDefault()
|
|
78
|
+
event.stopPropagation()
|
|
79
|
+
open()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
return button
|
|
83
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { type SelectedLineRange } from "@pierre/diffs"
|
|
2
|
+
import { diffLineIndex, diffRowIndex } from "./diff-selection"
|
|
3
|
+
|
|
4
|
+
export type CommentSide = "additions" | "deletions"
|
|
5
|
+
|
|
6
|
+
function annotationIndex(node: HTMLElement) {
|
|
7
|
+
const value = node.dataset.lineAnnotation?.split(",")[1]
|
|
8
|
+
if (!value) return
|
|
9
|
+
const line = parseInt(value, 10)
|
|
10
|
+
if (Number.isNaN(line)) return
|
|
11
|
+
return line
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function clear(root: ShadowRoot) {
|
|
15
|
+
const marked = Array.from(root.querySelectorAll("[data-comment-selected]"))
|
|
16
|
+
for (const node of marked) {
|
|
17
|
+
if (!(node instanceof HTMLElement)) continue
|
|
18
|
+
node.removeAttribute("data-comment-selected")
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function markCommentedDiffLines(root: ShadowRoot, ranges: SelectedLineRange[]) {
|
|
23
|
+
clear(root)
|
|
24
|
+
|
|
25
|
+
const diffs = root.querySelector("[data-diff]")
|
|
26
|
+
if (!(diffs instanceof HTMLElement)) return
|
|
27
|
+
|
|
28
|
+
const split = diffs.dataset.diffType === "split"
|
|
29
|
+
const rows = Array.from(diffs.querySelectorAll("[data-line-index]")).filter(
|
|
30
|
+
(node): node is HTMLElement => node instanceof HTMLElement,
|
|
31
|
+
)
|
|
32
|
+
if (rows.length === 0) return
|
|
33
|
+
|
|
34
|
+
const annotations = Array.from(diffs.querySelectorAll("[data-line-annotation]")).filter(
|
|
35
|
+
(node): node is HTMLElement => node instanceof HTMLElement,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
for (const range of ranges) {
|
|
39
|
+
const start = diffRowIndex(root, split, range.start, range.side as CommentSide | undefined)
|
|
40
|
+
if (start === undefined) continue
|
|
41
|
+
|
|
42
|
+
const end = (() => {
|
|
43
|
+
const same = range.end === range.start && (range.endSide == null || range.endSide === range.side)
|
|
44
|
+
if (same) return start
|
|
45
|
+
return diffRowIndex(root, split, range.end, (range.endSide ?? range.side) as CommentSide | undefined)
|
|
46
|
+
})()
|
|
47
|
+
if (end === undefined) continue
|
|
48
|
+
|
|
49
|
+
const first = Math.min(start, end)
|
|
50
|
+
const last = Math.max(start, end)
|
|
51
|
+
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
const idx = diffLineIndex(split, row)
|
|
54
|
+
if (idx === undefined || idx < first || idx > last) continue
|
|
55
|
+
row.setAttribute("data-comment-selected", "")
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const annotation of annotations) {
|
|
59
|
+
const idx = annotationIndex(annotation)
|
|
60
|
+
if (idx === undefined || idx < first || idx > last) continue
|
|
61
|
+
annotation.setAttribute("data-comment-selected", "")
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function markCommentedFileLines(root: ShadowRoot, ranges: SelectedLineRange[]) {
|
|
67
|
+
clear(root)
|
|
68
|
+
|
|
69
|
+
const annotations = Array.from(root.querySelectorAll("[data-line-annotation]")).filter(
|
|
70
|
+
(node): node is HTMLElement => node instanceof HTMLElement,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
for (const range of ranges) {
|
|
74
|
+
const start = Math.max(1, Math.min(range.start, range.end))
|
|
75
|
+
const end = Math.max(range.start, range.end)
|
|
76
|
+
|
|
77
|
+
for (let line = start; line <= end; line++) {
|
|
78
|
+
const nodes = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-column-number="${line}"]`))
|
|
79
|
+
for (const node of nodes) {
|
|
80
|
+
if (!(node instanceof HTMLElement)) continue
|
|
81
|
+
node.setAttribute("data-comment-selected", "")
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
for (const annotation of annotations) {
|
|
86
|
+
const line = annotationIndex(annotation)
|
|
87
|
+
if (line === undefined || line < start || line > end) continue
|
|
88
|
+
annotation.setAttribute("data-comment-selected", "")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type SelectedLineRange } from "@pierre/diffs"
|
|
2
|
+
|
|
3
|
+
export type DiffSelectionSide = "additions" | "deletions"
|
|
4
|
+
|
|
5
|
+
export function findDiffSide(node: HTMLElement): DiffSelectionSide {
|
|
6
|
+
const line = node.closest("[data-line], [data-alt-line]")
|
|
7
|
+
if (line instanceof HTMLElement) {
|
|
8
|
+
const type = line.dataset.lineType
|
|
9
|
+
if (type === "change-deletion") return "deletions"
|
|
10
|
+
if (type === "change-addition" || type === "change-additions") return "additions"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const code = node.closest("[data-code]")
|
|
14
|
+
if (!(code instanceof HTMLElement)) return "additions"
|
|
15
|
+
return code.hasAttribute("data-deletions") ? "deletions" : "additions"
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function diffLineIndex(split: boolean, node: HTMLElement) {
|
|
19
|
+
const raw = node.dataset.lineIndex
|
|
20
|
+
if (!raw) return
|
|
21
|
+
|
|
22
|
+
const values = raw
|
|
23
|
+
.split(",")
|
|
24
|
+
.map((x) => parseInt(x, 10))
|
|
25
|
+
.filter((x) => !Number.isNaN(x))
|
|
26
|
+
if (values.length === 0) return
|
|
27
|
+
if (!split) return values[0]
|
|
28
|
+
if (values.length === 2) return values[1]
|
|
29
|
+
return values[0]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function diffRowIndex(root: ShadowRoot, split: boolean, line: number, side: DiffSelectionSide | undefined) {
|
|
33
|
+
const rows = Array.from(root.querySelectorAll(`[data-line="${line}"], [data-alt-line="${line}"]`)).filter(
|
|
34
|
+
(node): node is HTMLElement => node instanceof HTMLElement,
|
|
35
|
+
)
|
|
36
|
+
if (rows.length === 0) return
|
|
37
|
+
|
|
38
|
+
const target = side ?? "additions"
|
|
39
|
+
for (const row of rows) {
|
|
40
|
+
if (findDiffSide(row) === target) return diffLineIndex(split, row)
|
|
41
|
+
if (parseInt(row.dataset.altLine ?? "", 10) === line) return diffLineIndex(split, row)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function fixDiffSelection(root: ShadowRoot | undefined, range: SelectedLineRange | null) {
|
|
46
|
+
if (!range) return range
|
|
47
|
+
if (!root) return
|
|
48
|
+
|
|
49
|
+
const diffs = root.querySelector("[data-diff]")
|
|
50
|
+
if (!(diffs instanceof HTMLElement)) return
|
|
51
|
+
|
|
52
|
+
const split = diffs.dataset.diffType === "split"
|
|
53
|
+
const start = diffRowIndex(root, split, range.start, range.side)
|
|
54
|
+
const end = diffRowIndex(root, split, range.end, range.endSide ?? range.side)
|
|
55
|
+
|
|
56
|
+
if (start === undefined || end === undefined) {
|
|
57
|
+
if (root.querySelector("[data-line], [data-alt-line]") == null) return
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
if (start <= end) return range
|
|
61
|
+
|
|
62
|
+
const side = range.endSide ?? range.side
|
|
63
|
+
const swapped: SelectedLineRange = {
|
|
64
|
+
start: range.end,
|
|
65
|
+
end: range.start,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (side) swapped.side = side
|
|
69
|
+
if (range.endSide && range.side) swapped.endSide = range.side
|
|
70
|
+
return swapped
|
|
71
|
+
}
|