@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,543 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AssistantMessage,
|
|
3
|
+
type SnapshotFileDiff,
|
|
4
|
+
Message as MessageType,
|
|
5
|
+
Part as PartType,
|
|
6
|
+
} from "@neurocode-ai/sdk/v2/client"
|
|
7
|
+
import type { FileDiffInfo } from "@neurocode-ai/client/promise"
|
|
8
|
+
import type { SessionStatus } from "@neurocode-ai/sdk/v2"
|
|
9
|
+
import { useData } from "../context"
|
|
10
|
+
import { useFileComponent } from "@neurocode-ai/ui/context/file"
|
|
11
|
+
|
|
12
|
+
import { Binary } from "@neurocode-ai/core/util/binary"
|
|
13
|
+
import { getDirectory, getFilename } from "@neurocode-ai/core/util/path"
|
|
14
|
+
import { createEffect, createMemo, createSignal, For, on, ParentProps, Show } from "solid-js"
|
|
15
|
+
import { createStore } from "solid-js/store"
|
|
16
|
+
import { Dynamic } from "solid-js/web"
|
|
17
|
+
import { AssistantParts, Message, MessageDivider, PART_MAPPING, type UserActions } from "./message-part"
|
|
18
|
+
import { Card } from "@neurocode-ai/ui/card"
|
|
19
|
+
import { Accordion } from "@neurocode-ai/ui/accordion"
|
|
20
|
+
import { StickyAccordionHeader } from "@neurocode-ai/ui/sticky-accordion-header"
|
|
21
|
+
import { DiffChanges } from "@neurocode-ai/ui/diff-changes"
|
|
22
|
+
import { Icon } from "@neurocode-ai/ui/icon"
|
|
23
|
+
import { TextShimmer } from "@neurocode-ai/ui/text-shimmer"
|
|
24
|
+
import { SessionRetry } from "./session-retry"
|
|
25
|
+
import { TextReveal } from "@neurocode-ai/ui/text-reveal"
|
|
26
|
+
import { createAutoScroll } from "@neurocode-ai/ui/hooks"
|
|
27
|
+
import { useI18n } from "@neurocode-ai/ui/context/i18n"
|
|
28
|
+
import { normalize } from "./session-diff"
|
|
29
|
+
|
|
30
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
31
|
+
return !!value && typeof value === "object" && !Array.isArray(value)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function unwrap(message: string) {
|
|
35
|
+
const text = message.replace(/^Error:\s*/, "").trim()
|
|
36
|
+
|
|
37
|
+
const parse = (value: string) => {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(value) as unknown
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const read = (value: string) => {
|
|
46
|
+
const first = parse(value)
|
|
47
|
+
if (typeof first !== "string") return first
|
|
48
|
+
return parse(first.trim())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let json = read(text)
|
|
52
|
+
|
|
53
|
+
if (json === undefined) {
|
|
54
|
+
const start = text.indexOf("{")
|
|
55
|
+
const end = text.lastIndexOf("}")
|
|
56
|
+
if (start !== -1 && end > start) {
|
|
57
|
+
json = read(text.slice(start, end + 1))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!record(json)) return message
|
|
62
|
+
|
|
63
|
+
const err = record(json.error) ? json.error : undefined
|
|
64
|
+
if (err) {
|
|
65
|
+
const type = typeof err.type === "string" ? err.type : undefined
|
|
66
|
+
const msg = typeof err.message === "string" ? err.message : undefined
|
|
67
|
+
if (type && msg) return `${type}: ${msg}`
|
|
68
|
+
if (msg) return msg
|
|
69
|
+
if (type) return type
|
|
70
|
+
const code = typeof err.code === "string" ? err.code : undefined
|
|
71
|
+
if (code) return code
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const msg = typeof json.message === "string" ? json.message : undefined
|
|
75
|
+
if (msg) return msg
|
|
76
|
+
|
|
77
|
+
const reason = typeof json.error === "string" ? json.error : undefined
|
|
78
|
+
if (reason) return reason
|
|
79
|
+
|
|
80
|
+
return message
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function same<T>(a: readonly T[], b: readonly T[]) {
|
|
84
|
+
if (a === b) return true
|
|
85
|
+
if (a.length !== b.length) return false
|
|
86
|
+
return a.every((x, i) => x === b[i])
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function list<T>(value: T[] | undefined | null, fallback: T[]) {
|
|
90
|
+
if (Array.isArray(value)) return value
|
|
91
|
+
return fallback
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
type SummaryDiff = (SnapshotFileDiff & { file: string }) | FileDiffInfo
|
|
95
|
+
|
|
96
|
+
function summaryDiff(value: SnapshotFileDiff): value is SummaryDiff {
|
|
97
|
+
return typeof value.file === "string"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const hidden = new Set(["todowrite"])
|
|
101
|
+
|
|
102
|
+
function partState(part: PartType, showReasoningSummaries: boolean) {
|
|
103
|
+
if (part.type === "tool") {
|
|
104
|
+
if (hidden.has(part.tool)) return
|
|
105
|
+
if (part.tool === "question" && (part.state.status === "pending" || part.state.status === "running")) return
|
|
106
|
+
return "visible" as const
|
|
107
|
+
}
|
|
108
|
+
if (part.type === "text") return part.text?.trim() ? ("visible" as const) : undefined
|
|
109
|
+
if (part.type === "reasoning") {
|
|
110
|
+
if (showReasoningSummaries && part.text?.trim()) return "visible" as const
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
if (PART_MAPPING[part.type]) return "visible" as const
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function clean(value: string) {
|
|
118
|
+
return value
|
|
119
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
120
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
121
|
+
.replace(/[*_~]+/g, "")
|
|
122
|
+
.trim()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function heading(text: string) {
|
|
126
|
+
const markdown = text.replace(/\r\n?/g, "\n")
|
|
127
|
+
|
|
128
|
+
const html = markdown.match(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/i)
|
|
129
|
+
if (html?.[1]) {
|
|
130
|
+
const value = clean(html[1].replace(/<[^>]+>/g, " "))
|
|
131
|
+
if (value) return value
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const atx = markdown.match(/^\s{0,3}#{1,6}[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/m)
|
|
135
|
+
if (atx?.[1]) {
|
|
136
|
+
const value = clean(atx[1])
|
|
137
|
+
if (value) return value
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const setext = markdown.match(/^([^\n]+)\n(?:=+|-+)\s*$/m)
|
|
141
|
+
if (setext?.[1]) {
|
|
142
|
+
const value = clean(setext[1])
|
|
143
|
+
if (value) return value
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const strong = markdown.match(/^\s*(?:\*\*|__)(.+?)(?:\*\*|__)\s*$/m)
|
|
147
|
+
if (strong?.[1]) {
|
|
148
|
+
const value = clean(strong[1])
|
|
149
|
+
if (value) return value
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function SessionTurn(
|
|
154
|
+
props: ParentProps<{
|
|
155
|
+
sessionID: string
|
|
156
|
+
messageID: string
|
|
157
|
+
messages?: MessageType[]
|
|
158
|
+
actions?: UserActions
|
|
159
|
+
showReasoningSummaries?: boolean
|
|
160
|
+
shellToolDefaultOpen?: boolean
|
|
161
|
+
editToolDefaultOpen?: boolean
|
|
162
|
+
active?: boolean
|
|
163
|
+
status?: SessionStatus
|
|
164
|
+
onUserInteracted?: () => void
|
|
165
|
+
classes?: {
|
|
166
|
+
root?: string
|
|
167
|
+
content?: string
|
|
168
|
+
container?: string
|
|
169
|
+
}
|
|
170
|
+
}>,
|
|
171
|
+
) {
|
|
172
|
+
const data = useData()
|
|
173
|
+
const i18n = useI18n()
|
|
174
|
+
const fileComponent = useFileComponent()
|
|
175
|
+
|
|
176
|
+
const emptyMessages: MessageType[] = []
|
|
177
|
+
const emptyParts: PartType[] = []
|
|
178
|
+
const emptyAssistant: AssistantMessage[] = []
|
|
179
|
+
const emptyDiffs: SummaryDiff[] = []
|
|
180
|
+
const idle = { type: "idle" as const }
|
|
181
|
+
|
|
182
|
+
const allMessages = createMemo(() => props.messages ?? list(data.store.message?.[props.sessionID], emptyMessages))
|
|
183
|
+
|
|
184
|
+
const messageIndex = createMemo(() => {
|
|
185
|
+
const messages = allMessages() ?? emptyMessages
|
|
186
|
+
const result = Binary.search(messages, props.messageID, (m) => m.id)
|
|
187
|
+
|
|
188
|
+
const index = result.found ? result.index : messages.findIndex((m) => m.id === props.messageID)
|
|
189
|
+
if (index < 0) return -1
|
|
190
|
+
|
|
191
|
+
const msg = messages[index]
|
|
192
|
+
if (!msg || msg.role !== "user") return -1
|
|
193
|
+
|
|
194
|
+
return index
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
const message = createMemo(() => {
|
|
198
|
+
const index = messageIndex()
|
|
199
|
+
if (index < 0) return undefined
|
|
200
|
+
|
|
201
|
+
const messages = allMessages() ?? emptyMessages
|
|
202
|
+
const msg = messages[index]
|
|
203
|
+
if (!msg || msg.role !== "user") return undefined
|
|
204
|
+
|
|
205
|
+
return msg
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const pending = createMemo(() => {
|
|
209
|
+
if (typeof props.active === "boolean") return
|
|
210
|
+
const messages = allMessages() ?? emptyMessages
|
|
211
|
+
return messages.findLast(
|
|
212
|
+
(item): item is AssistantMessage => item.role === "assistant" && typeof item.time.completed !== "number",
|
|
213
|
+
)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const pendingUser = createMemo(() => {
|
|
217
|
+
const item = pending()
|
|
218
|
+
if (!item?.parentID) return
|
|
219
|
+
const messages = allMessages() ?? emptyMessages
|
|
220
|
+
const result = Binary.search(messages, item.parentID, (m) => m.id)
|
|
221
|
+
const msg = result.found ? messages[result.index] : messages.find((m) => m.id === item.parentID)
|
|
222
|
+
if (!msg || msg.role !== "user") return
|
|
223
|
+
return msg
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const active = createMemo(() => {
|
|
227
|
+
if (typeof props.active === "boolean") return props.active
|
|
228
|
+
const msg = message()
|
|
229
|
+
const parent = pendingUser()
|
|
230
|
+
if (!msg || !parent) return false
|
|
231
|
+
return parent.id === msg.id
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const parts = createMemo(() => {
|
|
235
|
+
const msg = message()
|
|
236
|
+
if (!msg) return emptyParts
|
|
237
|
+
return list(data.store.part?.[msg.id], emptyParts)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const compaction = createMemo(() => parts().find((part) => part.type === "compaction"))
|
|
241
|
+
|
|
242
|
+
const diffs = createMemo(() => {
|
|
243
|
+
const files = message()?.summary?.diffs
|
|
244
|
+
if (!files?.length) return emptyDiffs
|
|
245
|
+
|
|
246
|
+
const seen = new Set<string>()
|
|
247
|
+
return files
|
|
248
|
+
.reduceRight<SummaryDiff[]>((result, diff) => {
|
|
249
|
+
if (!summaryDiff(diff)) return result
|
|
250
|
+
if (seen.has(diff.file)) return result
|
|
251
|
+
seen.add(diff.file)
|
|
252
|
+
result.push(diff)
|
|
253
|
+
return result
|
|
254
|
+
}, [])
|
|
255
|
+
.reverse()
|
|
256
|
+
})
|
|
257
|
+
const MAX_FILES = 10
|
|
258
|
+
const edited = createMemo(() => diffs().length)
|
|
259
|
+
const [state, setState] = createStore({
|
|
260
|
+
showAll: false,
|
|
261
|
+
expanded: [] as string[],
|
|
262
|
+
})
|
|
263
|
+
const showAll = () => state.showAll
|
|
264
|
+
const expanded = () => state.expanded
|
|
265
|
+
const overflow = createMemo(() => Math.max(0, edited() - MAX_FILES))
|
|
266
|
+
const visible = createMemo(() => (showAll() ? diffs() : diffs().slice(0, MAX_FILES)))
|
|
267
|
+
const toggleAll = () => {
|
|
268
|
+
autoScroll.pause()
|
|
269
|
+
setState("showAll", !showAll())
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const assistantMessages = createMemo(
|
|
273
|
+
() => {
|
|
274
|
+
const msg = message()
|
|
275
|
+
if (!msg) return emptyAssistant
|
|
276
|
+
|
|
277
|
+
const messages = allMessages() ?? emptyMessages
|
|
278
|
+
if (messageIndex() < 0) return emptyAssistant
|
|
279
|
+
|
|
280
|
+
const result: AssistantMessage[] = []
|
|
281
|
+
for (let i = 0; i < messages.length; i++) {
|
|
282
|
+
const item = messages[i]
|
|
283
|
+
if (!item) continue
|
|
284
|
+
if (item.role === "assistant" && item.parentID === msg.id) result.push(item as AssistantMessage)
|
|
285
|
+
}
|
|
286
|
+
return result
|
|
287
|
+
},
|
|
288
|
+
emptyAssistant,
|
|
289
|
+
{ equals: same },
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
const interrupted = createMemo(() => assistantMessages().some((m) => m.error?.name === "MessageAbortedError"))
|
|
293
|
+
const divider = createMemo(() => {
|
|
294
|
+
if (compaction()) return i18n.t("ui.messagePart.compaction")
|
|
295
|
+
if (interrupted()) return i18n.t("ui.message.interrupted")
|
|
296
|
+
return ""
|
|
297
|
+
})
|
|
298
|
+
const error = createMemo(
|
|
299
|
+
() => assistantMessages().find((m) => m.error && m.error.name !== "MessageAbortedError")?.error,
|
|
300
|
+
)
|
|
301
|
+
const showAssistantCopyPartID = createMemo(() => {
|
|
302
|
+
const messages = assistantMessages()
|
|
303
|
+
|
|
304
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
305
|
+
const message = messages[i]
|
|
306
|
+
if (!message) continue
|
|
307
|
+
|
|
308
|
+
const parts = list(data.store.part?.[message.id], emptyParts)
|
|
309
|
+
for (let j = parts.length - 1; j >= 0; j--) {
|
|
310
|
+
const part = parts[j]
|
|
311
|
+
if (!part || part.type !== "text" || !part.text?.trim()) continue
|
|
312
|
+
return part.id
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return undefined
|
|
317
|
+
})
|
|
318
|
+
const errorText = createMemo(() => {
|
|
319
|
+
const msg = error()?.data?.message
|
|
320
|
+
if (typeof msg === "string") return unwrap(msg)
|
|
321
|
+
if (msg === undefined || msg === null) return ""
|
|
322
|
+
// oxlint-disable-next-line no-base-to-string -- msg is unknown from error data, coercion is intentional
|
|
323
|
+
return unwrap(String(msg))
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
const status = createMemo(() => {
|
|
327
|
+
if (props.status !== undefined) return props.status
|
|
328
|
+
if (typeof props.active === "boolean" && !props.active) return idle
|
|
329
|
+
return data.store.session_status[props.sessionID] ?? idle
|
|
330
|
+
})
|
|
331
|
+
const working = createMemo(() => status().type !== "idle" && active())
|
|
332
|
+
const showReasoningSummaries = createMemo(() => props.showReasoningSummaries ?? true)
|
|
333
|
+
|
|
334
|
+
const assistantCopyPartID = createMemo(() => {
|
|
335
|
+
if (working()) return null
|
|
336
|
+
return showAssistantCopyPartID() ?? null
|
|
337
|
+
})
|
|
338
|
+
const turnDurationMs = createMemo(() => {
|
|
339
|
+
const start = message()?.time.created
|
|
340
|
+
if (typeof start !== "number") return undefined
|
|
341
|
+
|
|
342
|
+
const end = assistantMessages().reduce<number | undefined>((max, item) => {
|
|
343
|
+
const completed = item.time.completed
|
|
344
|
+
if (typeof completed !== "number") return max
|
|
345
|
+
if (max === undefined) return completed
|
|
346
|
+
return Math.max(max, completed)
|
|
347
|
+
}, undefined)
|
|
348
|
+
|
|
349
|
+
if (typeof end !== "number") return undefined
|
|
350
|
+
if (end < start) return undefined
|
|
351
|
+
return end - start
|
|
352
|
+
})
|
|
353
|
+
const assistantDerived = createMemo(() => {
|
|
354
|
+
let visible = 0
|
|
355
|
+
let reason: string | undefined
|
|
356
|
+
const show = showReasoningSummaries()
|
|
357
|
+
for (const message of assistantMessages()) {
|
|
358
|
+
for (const part of list(data.store.part?.[message.id], emptyParts)) {
|
|
359
|
+
if (partState(part, show) === "visible") {
|
|
360
|
+
visible++
|
|
361
|
+
}
|
|
362
|
+
if (part.type === "reasoning" && part.text) {
|
|
363
|
+
const h = heading(part.text)
|
|
364
|
+
if (h) reason = h
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return { visible, reason }
|
|
369
|
+
})
|
|
370
|
+
const assistantVisible = createMemo(() => assistantDerived().visible)
|
|
371
|
+
const reasoningHeading = createMemo(() => assistantDerived().reason)
|
|
372
|
+
const showThinking = createMemo(() => {
|
|
373
|
+
if (!working() || !!error()) return false
|
|
374
|
+
if (status().type === "retry") return false
|
|
375
|
+
if (showReasoningSummaries()) return assistantVisible() === 0
|
|
376
|
+
return true
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const autoScroll = createAutoScroll({
|
|
380
|
+
working,
|
|
381
|
+
onUserInteracted: props.onUserInteracted,
|
|
382
|
+
overflowAnchor: "dynamic",
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
return (
|
|
386
|
+
<div data-component="session-turn" class={props.classes?.root}>
|
|
387
|
+
<div
|
|
388
|
+
ref={autoScroll.scrollRef}
|
|
389
|
+
onScroll={autoScroll.handleScroll}
|
|
390
|
+
data-slot="session-turn-content"
|
|
391
|
+
class={props.classes?.content}
|
|
392
|
+
>
|
|
393
|
+
<div onClick={autoScroll.handleInteraction}>
|
|
394
|
+
<Show when={message()}>
|
|
395
|
+
<div
|
|
396
|
+
ref={autoScroll.contentRef}
|
|
397
|
+
data-message={message()!.id}
|
|
398
|
+
data-slot="session-turn-message-container"
|
|
399
|
+
class={props.classes?.container}
|
|
400
|
+
>
|
|
401
|
+
<div data-slot="session-turn-message-content" aria-live="off">
|
|
402
|
+
<Message message={message()!} parts={parts()} actions={props.actions} />
|
|
403
|
+
</div>
|
|
404
|
+
<Show when={divider()}>
|
|
405
|
+
<div data-slot="session-turn-compaction">
|
|
406
|
+
<MessageDivider label={divider()} />
|
|
407
|
+
</div>
|
|
408
|
+
</Show>
|
|
409
|
+
<Show when={assistantMessages().length > 0}>
|
|
410
|
+
<div data-slot="session-turn-assistant-content" aria-hidden={working()}>
|
|
411
|
+
<AssistantParts
|
|
412
|
+
messages={assistantMessages()}
|
|
413
|
+
showAssistantCopyPartID={assistantCopyPartID()}
|
|
414
|
+
turnDurationMs={turnDurationMs()}
|
|
415
|
+
working={working()}
|
|
416
|
+
showReasoningSummaries={showReasoningSummaries()}
|
|
417
|
+
shellToolDefaultOpen={props.shellToolDefaultOpen}
|
|
418
|
+
editToolDefaultOpen={props.editToolDefaultOpen}
|
|
419
|
+
/>
|
|
420
|
+
</div>
|
|
421
|
+
</Show>
|
|
422
|
+
<Show when={showThinking()}>
|
|
423
|
+
<div data-slot="session-turn-thinking">
|
|
424
|
+
<TextShimmer text={i18n.t("ui.sessionTurn.status.thinking")} />
|
|
425
|
+
<Show when={!showReasoningSummaries()}>
|
|
426
|
+
<TextReveal
|
|
427
|
+
text={reasoningHeading()}
|
|
428
|
+
class="session-turn-thinking-heading"
|
|
429
|
+
travel={25}
|
|
430
|
+
duration={700}
|
|
431
|
+
/>
|
|
432
|
+
</Show>
|
|
433
|
+
</div>
|
|
434
|
+
</Show>
|
|
435
|
+
<SessionRetry status={status()} show={active()} />
|
|
436
|
+
<Show when={edited() > 0 && !working()}>
|
|
437
|
+
<div
|
|
438
|
+
data-slot="session-turn-diffs"
|
|
439
|
+
data-component="session-turn-diffs-group"
|
|
440
|
+
data-show-all={showAll() || undefined}
|
|
441
|
+
>
|
|
442
|
+
<div data-slot="session-turn-diffs-header">
|
|
443
|
+
<span data-slot="session-turn-diffs-label">
|
|
444
|
+
{i18n.t(
|
|
445
|
+
edited() === 1 ? "ui.sessionTurn.diffs.changed.one" : "ui.sessionTurn.diffs.changed.other",
|
|
446
|
+
{ count: String(edited()) },
|
|
447
|
+
)}
|
|
448
|
+
</span>
|
|
449
|
+
<DiffChanges changes={diffs()} />
|
|
450
|
+
<Show when={overflow() > 0}>
|
|
451
|
+
<span data-slot="session-turn-diffs-toggle" onClick={toggleAll}>
|
|
452
|
+
{showAll() ? i18n.t("ui.sessionTurn.diffs.showLess") : i18n.t("ui.sessionTurn.diffs.showAll")}
|
|
453
|
+
</span>
|
|
454
|
+
</Show>
|
|
455
|
+
</div>
|
|
456
|
+
<div data-component="session-turn-diffs-content">
|
|
457
|
+
<Accordion
|
|
458
|
+
multiple
|
|
459
|
+
style={{ "--sticky-accordion-offset": "44px" }}
|
|
460
|
+
value={expanded()}
|
|
461
|
+
onChange={(value) => setState("expanded", Array.isArray(value) ? value : value ? [value] : [])}
|
|
462
|
+
>
|
|
463
|
+
<For each={visible()}>
|
|
464
|
+
{(diff) => {
|
|
465
|
+
const view = normalize(diff)
|
|
466
|
+
const active = createMemo(() => expanded().includes(diff.file))
|
|
467
|
+
const [shown, setShown] = createSignal(false)
|
|
468
|
+
|
|
469
|
+
createEffect(
|
|
470
|
+
on(
|
|
471
|
+
active,
|
|
472
|
+
(value) => {
|
|
473
|
+
if (!value) {
|
|
474
|
+
setShown(false)
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
requestAnimationFrame(() => {
|
|
479
|
+
if (!active()) return
|
|
480
|
+
setShown(true)
|
|
481
|
+
})
|
|
482
|
+
},
|
|
483
|
+
{ defer: true },
|
|
484
|
+
),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
return (
|
|
488
|
+
<Accordion.Item value={diff.file}>
|
|
489
|
+
<StickyAccordionHeader>
|
|
490
|
+
<Accordion.Trigger>
|
|
491
|
+
<div data-slot="session-turn-diff-trigger">
|
|
492
|
+
<span data-slot="session-turn-diff-path">
|
|
493
|
+
<Show when={diff.file.includes("/")}>
|
|
494
|
+
<span data-slot="session-turn-diff-directory">
|
|
495
|
+
{`\u202A${getDirectory(diff.file)}\u202C`}
|
|
496
|
+
</span>
|
|
497
|
+
</Show>
|
|
498
|
+
<span data-slot="session-turn-diff-filename">{getFilename(diff.file)}</span>
|
|
499
|
+
</span>
|
|
500
|
+
<div data-slot="session-turn-diff-meta">
|
|
501
|
+
<span data-slot="session-turn-diff-changes">
|
|
502
|
+
<DiffChanges changes={diff} />
|
|
503
|
+
</span>
|
|
504
|
+
<span data-slot="session-turn-diff-chevron">
|
|
505
|
+
<Icon name="chevron-down" size="small" />
|
|
506
|
+
</span>
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</Accordion.Trigger>
|
|
510
|
+
</StickyAccordionHeader>
|
|
511
|
+
<Accordion.Content>
|
|
512
|
+
<Show when={shown()}>
|
|
513
|
+
<div data-slot="session-turn-diff-view" data-scrollable>
|
|
514
|
+
<Dynamic component={fileComponent} mode="diff" fileDiff={view.fileDiff} />
|
|
515
|
+
</div>
|
|
516
|
+
</Show>
|
|
517
|
+
</Accordion.Content>
|
|
518
|
+
</Accordion.Item>
|
|
519
|
+
)
|
|
520
|
+
}}
|
|
521
|
+
</For>
|
|
522
|
+
</Accordion>
|
|
523
|
+
<Show when={!showAll() && overflow() > 0}>
|
|
524
|
+
<div data-slot="session-turn-diffs-more" onClick={toggleAll}>
|
|
525
|
+
{i18n.t("ui.sessionTurn.diffs.more", { count: String(overflow()) })}
|
|
526
|
+
</div>
|
|
527
|
+
</Show>
|
|
528
|
+
</div>
|
|
529
|
+
</div>
|
|
530
|
+
</Show>
|
|
531
|
+
<Show when={error()}>
|
|
532
|
+
<Card variant="error" class="error-card">
|
|
533
|
+
{errorText()}
|
|
534
|
+
</Card>
|
|
535
|
+
</Show>
|
|
536
|
+
</div>
|
|
537
|
+
</Show>
|
|
538
|
+
{props.children}
|
|
539
|
+
</div>
|
|
540
|
+
</div>
|
|
541
|
+
</div>
|
|
542
|
+
)
|
|
543
|
+
}
|