mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-13 15:44:56 +00:00
Apply PR #26949: perf(app): virtualize session timeline rows
This commit is contained in:
4
bun.lock
4
bun.lock
@@ -764,7 +764,7 @@
|
|||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"typescript": "5.8.2",
|
"typescript": "5.8.2",
|
||||||
"ulid": "3.0.1",
|
"ulid": "3.0.1",
|
||||||
"virtua": "0.42.3",
|
"virtua": "0.49.1",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.4",
|
||||||
"vite-plugin-solid": "2.11.10",
|
"vite-plugin-solid": "2.11.10",
|
||||||
"zod": "4.1.8",
|
"zod": "4.1.8",
|
||||||
@@ -4904,7 +4904,7 @@
|
|||||||
|
|
||||||
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
|
||||||
|
|
||||||
"virtua": ["virtua@0.42.3", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-5FoAKcEvh05qsUF97Yz42SWJ7bwnPExjUYHGuoxz1EUtfWtaOgXaRwnylJbDpA0QcH1rKvJ2qsGRi9MK1fpQbg=="],
|
"virtua": ["virtua@0.49.1", "", { "peerDependencies": { "react": ">=16.14.0", "react-dom": ">=16.14.0", "solid-js": ">=1.0", "svelte": ">=5.0", "vue": ">=3.2" }, "optionalPeers": ["react", "react-dom", "solid-js", "svelte", "vue"] }, "sha512-6f79msqg3jzNFdqJiS0FSzhRN1EHlDhR7EvW7emp6z5qQ22VdsReiDHflkpMEMhoAyUuYr69nwT0aagiM7NrUg=="],
|
||||||
|
|
||||||
"vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="],
|
"vite": ["vite@7.1.4", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-X5QFK4SGynAeeIt+A7ZWnApdUyHYm+pzv/8/A57LqSGcI88U6R6ipOs3uCesdc6yl7nl+zNO0t8LmqAdXcQihw=="],
|
||||||
|
|
||||||
|
|||||||
@@ -74,7 +74,7 @@
|
|||||||
"shiki": "3.20.0",
|
"shiki": "3.20.0",
|
||||||
"solid-list": "0.3.0",
|
"solid-list": "0.3.0",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"virtua": "0.42.3",
|
"virtua": "0.49.1",
|
||||||
"vite": "7.1.4",
|
"vite": "7.1.4",
|
||||||
"@solidjs/meta": "0.29.4",
|
"@solidjs/meta": "0.29.4",
|
||||||
"@solidjs/router": "0.15.4",
|
"@solidjs/router": "0.15.4",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { previewSelectedLines } from "@opencode-ai/ui/pierre/selection-bridge"
|
|||||||
import { Button } from "@opencode-ai/ui/button"
|
import { Button } from "@opencode-ai/ui/button"
|
||||||
import { showToast } from "@opencode-ai/ui/toast"
|
import { showToast } from "@opencode-ai/ui/toast"
|
||||||
import { checksum } from "@opencode-ai/core/util/encode"
|
import { checksum } from "@opencode-ai/core/util/encode"
|
||||||
import { useSearchParams } from "@solidjs/router"
|
import { useLocation, useSearchParams } from "@solidjs/router"
|
||||||
import { NewSessionView, SessionHeader } from "@/components/session"
|
import { NewSessionView, SessionHeader } from "@/components/session"
|
||||||
import { useComments } from "@/context/comments"
|
import { useComments } from "@/context/comments"
|
||||||
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
import { getSessionPrefetch, SESSION_PREFETCH_TTL } from "@/context/global-sync/session-prefetch"
|
||||||
@@ -75,7 +75,6 @@ type VcsMode = "git" | "branch"
|
|||||||
|
|
||||||
type SessionHistoryWindowInput = {
|
type SessionHistoryWindowInput = {
|
||||||
sessionID: () => string | undefined
|
sessionID: () => string | undefined
|
||||||
messagesReady: () => boolean
|
|
||||||
loaded: () => number
|
loaded: () => number
|
||||||
visibleUserMessages: () => UserMessage[]
|
visibleUserMessages: () => UserMessage[]
|
||||||
historyMore: () => boolean
|
historyMore: () => boolean
|
||||||
@@ -85,205 +84,78 @@ type SessionHistoryWindowInput = {
|
|||||||
scroller: () => HTMLDivElement | undefined
|
scroller: () => HTMLDivElement | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function createSessionHistoryLoader(input: SessionHistoryWindowInput) {
|
||||||
* Maintains the rendered history window for a session timeline.
|
const historyScrollThreshold = 200
|
||||||
*
|
let shiftFrame: number | undefined
|
||||||
* It keeps initial paint bounded to recent turns, reveals cached turns in
|
|
||||||
* small batches while scrolling upward, and prefetches older history near top.
|
|
||||||
*/
|
|
||||||
function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|
||||||
const turnInit = 10
|
|
||||||
const turnBatch = 8
|
|
||||||
const turnScrollThreshold = 200
|
|
||||||
const turnPrefetchBuffer = 16
|
|
||||||
const prefetchCooldownMs = 400
|
|
||||||
const prefetchNoGrowthLimit = 2
|
|
||||||
|
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
turnID: undefined as string | undefined,
|
shift: false,
|
||||||
turnStart: 0,
|
|
||||||
prefetchUntil: 0,
|
|
||||||
prefetchNoGrowth: 0,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialTurnStart = (len: number) => (len > turnInit ? len - turnInit : 0)
|
const userMessages = createMemo(
|
||||||
|
() => input.visibleUserMessages(),
|
||||||
const turnStart = createMemo(() => {
|
|
||||||
const id = input.sessionID()
|
|
||||||
const len = input.visibleUserMessages().length
|
|
||||||
if (!id || len <= 0) return 0
|
|
||||||
if (state.turnID !== id) return initialTurnStart(len)
|
|
||||||
if (state.turnStart <= 0) return 0
|
|
||||||
if (state.turnStart >= len) return initialTurnStart(len)
|
|
||||||
return state.turnStart
|
|
||||||
})
|
|
||||||
|
|
||||||
const setTurnStart = (start: number) => {
|
|
||||||
const id = input.sessionID()
|
|
||||||
const next = start > 0 ? start : 0
|
|
||||||
if (!id) {
|
|
||||||
setState({ turnID: undefined, turnStart: next })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
setState({ turnID: id, turnStart: next })
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderedUserMessages = createMemo(
|
|
||||||
() => {
|
|
||||||
const msgs = input.visibleUserMessages()
|
|
||||||
const start = turnStart()
|
|
||||||
if (start <= 0) return msgs
|
|
||||||
return msgs.slice(start)
|
|
||||||
},
|
|
||||||
emptyUserMessages,
|
emptyUserMessages,
|
||||||
{
|
{
|
||||||
equals: same,
|
equals: same,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const preserveScroll = (fn: () => void) => {
|
const cancelShiftReset = () => {
|
||||||
const el = input.scroller()
|
if (shiftFrame === undefined) return
|
||||||
if (!el) {
|
cancelAnimationFrame(shiftFrame)
|
||||||
fn()
|
shiftFrame = undefined
|
||||||
return
|
}
|
||||||
}
|
|
||||||
const beforeTop = el.scrollTop
|
const scheduleShiftReset = () => {
|
||||||
const beforeHeight = el.scrollHeight
|
cancelShiftReset()
|
||||||
fn()
|
shiftFrame = requestAnimationFrame(() => {
|
||||||
requestAnimationFrame(() => {
|
shiftFrame = undefined
|
||||||
const delta = el.scrollHeight - beforeHeight
|
setState("shift", false)
|
||||||
if (!delta) return
|
|
||||||
el.scrollTop = beforeTop + delta
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const backfillTurns = () => {
|
const fetchOlderMessages = async () => {
|
||||||
const start = turnStart()
|
|
||||||
if (start <= 0) return
|
|
||||||
|
|
||||||
const next = start - turnBatch
|
|
||||||
const nextStart = next > 0 ? next : 0
|
|
||||||
|
|
||||||
preserveScroll(() => setTurnStart(nextStart))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Button path: reveal all cached turns, fetch older history, reveal one batch. */
|
|
||||||
const loadAndReveal = async () => {
|
|
||||||
const id = input.sessionID()
|
|
||||||
if (!id) return
|
|
||||||
|
|
||||||
const start = turnStart()
|
|
||||||
const beforeVisible = input.visibleUserMessages().length
|
|
||||||
let loaded = input.loaded()
|
|
||||||
|
|
||||||
if (start > 0) setTurnStart(0)
|
|
||||||
|
|
||||||
if (!input.historyMore() || input.historyLoading()) return
|
|
||||||
|
|
||||||
let afterVisible = beforeVisible
|
|
||||||
let added = 0
|
|
||||||
|
|
||||||
while (true) {
|
|
||||||
await input.loadMore(id)
|
|
||||||
if (input.sessionID() !== id) return
|
|
||||||
|
|
||||||
afterVisible = input.visibleUserMessages().length
|
|
||||||
const nextLoaded = input.loaded()
|
|
||||||
const raw = nextLoaded - loaded
|
|
||||||
added += raw
|
|
||||||
loaded = nextLoaded
|
|
||||||
|
|
||||||
if (afterVisible > beforeVisible) break
|
|
||||||
if (raw <= 0) break
|
|
||||||
if (!input.historyMore()) break
|
|
||||||
}
|
|
||||||
|
|
||||||
if (added <= 0) return
|
|
||||||
if (state.prefetchNoGrowth) setState("prefetchNoGrowth", 0)
|
|
||||||
|
|
||||||
const growth = afterVisible - beforeVisible
|
|
||||||
if (growth <= 0) return
|
|
||||||
if (turnStart() !== 0) return
|
|
||||||
|
|
||||||
const target = Math.min(afterVisible, beforeVisible + turnBatch)
|
|
||||||
setTurnStart(Math.max(0, afterVisible - target))
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Scroll/prefetch path: fetch older history from server. */
|
|
||||||
const fetchOlderMessages = async (opts?: { prefetch?: boolean }) => {
|
|
||||||
const id = input.sessionID()
|
const id = input.sessionID()
|
||||||
if (!id) return
|
if (!id) return
|
||||||
if (!input.historyMore() || input.historyLoading()) return
|
if (!input.historyMore() || input.historyLoading()) return
|
||||||
|
|
||||||
if (opts?.prefetch) {
|
// TODO(session-timeline): switch this to core cursor-based part pagination when that API lands.
|
||||||
const now = Date.now()
|
|
||||||
if (state.prefetchUntil > now) return
|
|
||||||
if (state.prefetchNoGrowth >= prefetchNoGrowthLimit) return
|
|
||||||
setState("prefetchUntil", now + prefetchCooldownMs)
|
|
||||||
}
|
|
||||||
|
|
||||||
const start = turnStart()
|
|
||||||
const beforeVisible = input.visibleUserMessages().length
|
const beforeVisible = input.visibleUserMessages().length
|
||||||
const beforeRendered = start <= 0 ? beforeVisible : renderedUserMessages().length
|
|
||||||
let loaded = input.loaded()
|
let loaded = input.loaded()
|
||||||
let added = 0
|
|
||||||
let growth = 0
|
let growth = 0
|
||||||
|
|
||||||
|
cancelShiftReset()
|
||||||
|
setState("shift", true)
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
await input.loadMore(id)
|
await input.loadMore(id)
|
||||||
if (input.sessionID() !== id) return
|
if (input.sessionID() !== id) return
|
||||||
|
|
||||||
const nextLoaded = input.loaded()
|
const nextLoaded = input.loaded()
|
||||||
const raw = nextLoaded - loaded
|
const raw = nextLoaded - loaded
|
||||||
added += raw
|
|
||||||
loaded = nextLoaded
|
loaded = nextLoaded
|
||||||
growth = input.visibleUserMessages().length - beforeVisible
|
growth = input.visibleUserMessages().length - beforeVisible
|
||||||
|
|
||||||
if (growth > 0) break
|
if (growth > 0) break
|
||||||
if (raw <= 0) break
|
if (raw <= 0) break
|
||||||
if (opts?.prefetch) break
|
|
||||||
if (!input.historyMore()) break
|
if (!input.historyMore()) break
|
||||||
}
|
}
|
||||||
|
|
||||||
const afterVisible = input.visibleUserMessages().length
|
if (growth > 0) {
|
||||||
|
scheduleShiftReset()
|
||||||
if (opts?.prefetch) {
|
|
||||||
setState("prefetchNoGrowth", added > 0 ? 0 : state.prefetchNoGrowth + 1)
|
|
||||||
} else if (added > 0 && state.prefetchNoGrowth) {
|
|
||||||
setState("prefetchNoGrowth", 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (added <= 0) return
|
|
||||||
if (growth <= 0) return
|
|
||||||
|
|
||||||
if (opts?.prefetch) {
|
|
||||||
const current = turnStart()
|
|
||||||
preserveScroll(() => setTurnStart(current + growth))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (turnStart() !== start) return
|
setState("shift", false)
|
||||||
|
|
||||||
const currentRendered = renderedUserMessages().length
|
|
||||||
const base = Math.max(beforeRendered, currentRendered)
|
|
||||||
const target = Math.min(afterVisible, base + turnBatch)
|
|
||||||
preserveScroll(() => setTurnStart(Math.max(0, afterVisible - target)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadAndReveal = () => fetchOlderMessages()
|
||||||
|
|
||||||
const onScrollerScroll = () => {
|
const onScrollerScroll = () => {
|
||||||
if (!input.userScrolled()) return
|
if (!input.userScrolled()) return
|
||||||
const el = input.scroller()
|
const el = input.scroller()
|
||||||
if (!el) return
|
if (!el) return
|
||||||
if (el.scrollTop >= turnScrollThreshold) return
|
if (el.scrollTop >= historyScrollThreshold) return
|
||||||
|
|
||||||
const start = turnStart()
|
|
||||||
if (start > 0) {
|
|
||||||
if (start <= turnPrefetchBuffer) {
|
|
||||||
void fetchOlderMessages({ prefetch: true })
|
|
||||||
}
|
|
||||||
backfillTurns()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
void fetchOlderMessages()
|
void fetchOlderMessages()
|
||||||
}
|
}
|
||||||
@@ -292,27 +164,18 @@ function createSessionHistoryWindow(input: SessionHistoryWindowInput) {
|
|||||||
on(
|
on(
|
||||||
input.sessionID,
|
input.sessionID,
|
||||||
() => {
|
() => {
|
||||||
setState({ prefetchUntil: 0, prefetchNoGrowth: 0 })
|
cancelShiftReset()
|
||||||
|
setState({ shift: false })
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
createEffect(
|
onCleanup(cancelShiftReset)
|
||||||
on(
|
|
||||||
() => [input.sessionID(), input.messagesReady()] as const,
|
|
||||||
([id, ready]) => {
|
|
||||||
if (!id || !ready) return
|
|
||||||
setTurnStart(initialTurnStart(input.visibleUserMessages().length))
|
|
||||||
},
|
|
||||||
{ defer: true },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
turnStart,
|
userMessages,
|
||||||
setTurnStart,
|
shift: () => state.shift,
|
||||||
renderedUserMessages,
|
|
||||||
loadAndReveal,
|
loadAndReveal,
|
||||||
onScrollerScroll,
|
onScrollerScroll,
|
||||||
}
|
}
|
||||||
@@ -333,6 +196,7 @@ export default function Page() {
|
|||||||
const comments = useComments()
|
const comments = useComments()
|
||||||
const terminal = useTerminal()
|
const terminal = useTerminal()
|
||||||
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
const [searchParams, setSearchParams] = useSearchParams<{ prompt?: string }>()
|
||||||
|
const location = useLocation()
|
||||||
const { params, sessionKey, tabs, view } = useSessionLayout()
|
const { params, sessionKey, tabs, view } = useSessionLayout()
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
@@ -737,6 +601,7 @@ export default function Page() {
|
|||||||
let dockHeight = 0
|
let dockHeight = 0
|
||||||
let scroller: HTMLDivElement | undefined
|
let scroller: HTMLDivElement | undefined
|
||||||
let content: HTMLDivElement | undefined
|
let content: HTMLDivElement | undefined
|
||||||
|
let revealMessage = (_id: string) => {}
|
||||||
let scrollMark = 0
|
let scrollMark = 0
|
||||||
let messageMark = 0
|
let messageMark = 0
|
||||||
|
|
||||||
@@ -1403,9 +1268,8 @@ export default function Page() {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const historyWindow = createSessionHistoryWindow({
|
const historyLoader = createSessionHistoryLoader({
|
||||||
sessionID: () => params.id,
|
sessionID: () => params.id,
|
||||||
messagesReady,
|
|
||||||
loaded: () => messages().length,
|
loaded: () => messages().length,
|
||||||
visibleUserMessages,
|
visibleUserMessages,
|
||||||
historyMore,
|
historyMore,
|
||||||
@@ -1427,9 +1291,9 @@ export default function Page() {
|
|||||||
const el = scroller
|
const el = scroller
|
||||||
if (!el) return
|
if (!el) return
|
||||||
if (el.scrollHeight > el.clientHeight + 1) return
|
if (el.scrollHeight > el.clientHeight + 1) return
|
||||||
if (historyWindow.turnStart() <= 0 && !historyMore()) return
|
if (!historyMore()) return
|
||||||
|
|
||||||
void historyWindow.loadAndReveal()
|
void historyLoader.loadAndReveal()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1439,15 +1303,14 @@ export default function Page() {
|
|||||||
[
|
[
|
||||||
params.id,
|
params.id,
|
||||||
messagesReady(),
|
messagesReady(),
|
||||||
historyWindow.turnStart(),
|
|
||||||
historyMore(),
|
historyMore(),
|
||||||
historyLoading(),
|
historyLoading(),
|
||||||
autoScroll.userScrolled(),
|
autoScroll.userScrolled(),
|
||||||
visibleUserMessages().length,
|
visibleUserMessages().length,
|
||||||
] as const,
|
] as const,
|
||||||
([id, ready, start, more, loading, scrolled]) => {
|
([id, ready, more, loading, scrolled]) => {
|
||||||
if (!id || !ready || loading || scrolled) return
|
if (!id || !ready || loading || scrolled) return
|
||||||
if (start <= 0 && !more) return
|
if (!more) return
|
||||||
fill()
|
fill()
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
@@ -1749,15 +1612,14 @@ export default function Page() {
|
|||||||
historyMore,
|
historyMore,
|
||||||
historyLoading,
|
historyLoading,
|
||||||
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
|
loadMore: (sessionID) => sync.session.history.loadMore(sessionID),
|
||||||
turnStart: historyWindow.turnStart,
|
|
||||||
currentMessageId: () => store.messageId,
|
currentMessageId: () => store.messageId,
|
||||||
pendingMessage: () => ui.pendingMessage,
|
pendingMessage: () => ui.pendingMessage,
|
||||||
setPendingMessage: (value) => setUi("pendingMessage", value),
|
setPendingMessage: (value) => setUi("pendingMessage", value),
|
||||||
setActiveMessage,
|
setActiveMessage,
|
||||||
setTurnStart: historyWindow.setTurnStart,
|
|
||||||
autoScroll,
|
autoScroll,
|
||||||
scroller: () => scroller,
|
scroller: () => scroller,
|
||||||
anchor,
|
anchor,
|
||||||
|
revealMessage: (id) => revealMessage(id),
|
||||||
scheduleScrollState,
|
scheduleScrollState,
|
||||||
consumePendingMessage: layout.pendingMessage.consume,
|
consumePendingMessage: layout.pendingMessage.consume,
|
||||||
})
|
})
|
||||||
@@ -1831,7 +1693,7 @@ export default function Page() {
|
|||||||
<div class="flex-1 min-h-0 overflow-hidden">
|
<div class="flex-1 min-h-0 overflow-hidden">
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={params.id}>
|
<Match when={params.id}>
|
||||||
<Show when={messagesReady()}>
|
<Show when={!store.deferRender && messagesReady()}>
|
||||||
<MessageTimeline
|
<MessageTimeline
|
||||||
mobileChanges={mobileChanges()}
|
mobileChanges={mobileChanges()}
|
||||||
mobileFallback={reviewContent({
|
mobileFallback={reviewContent({
|
||||||
@@ -1853,8 +1715,11 @@ export default function Page() {
|
|||||||
onMarkScrollGesture={markScrollGesture}
|
onMarkScrollGesture={markScrollGesture}
|
||||||
hasScrollGesture={hasScrollGesture}
|
hasScrollGesture={hasScrollGesture}
|
||||||
onUserScroll={markUserScroll}
|
onUserScroll={markUserScroll}
|
||||||
onTurnBackfillScroll={historyWindow.onScrollerScroll}
|
onHistoryScroll={historyLoader.onScrollerScroll}
|
||||||
onAutoScrollInteraction={autoScroll.handleInteraction}
|
onAutoScrollInteraction={autoScroll.handleInteraction}
|
||||||
|
shouldAnchorBottom={() =>
|
||||||
|
!location.hash && !store.messageId && !ui.pendingMessage && !autoScroll.userScrolled()
|
||||||
|
}
|
||||||
centered={centered()}
|
centered={centered()}
|
||||||
setContentRef={(el) => {
|
setContentRef={(el) => {
|
||||||
content = el
|
content = el
|
||||||
@@ -1863,14 +1728,12 @@ export default function Page() {
|
|||||||
const root = scroller
|
const root = scroller
|
||||||
if (root) scheduleScrollState(root)
|
if (root) scheduleScrollState(root)
|
||||||
}}
|
}}
|
||||||
turnStart={historyWindow.turnStart()}
|
historyShift={historyLoader.shift()}
|
||||||
historyMore={historyMore()}
|
userMessages={historyLoader.userMessages()}
|
||||||
historyLoading={historyLoading()}
|
|
||||||
onLoadEarlier={() => {
|
|
||||||
void historyWindow.loadAndReveal()
|
|
||||||
}}
|
|
||||||
renderedUserMessages={historyWindow.renderedUserMessages()}
|
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
|
setRevealMessage={(fn) => {
|
||||||
|
revealMessage = fn
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
</Match>
|
</Match>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -11,21 +11,19 @@ export const useSessionHashScroll = (input: {
|
|||||||
historyMore: () => boolean
|
historyMore: () => boolean
|
||||||
historyLoading: () => boolean
|
historyLoading: () => boolean
|
||||||
loadMore: (sessionID: string) => Promise<void>
|
loadMore: (sessionID: string) => Promise<void>
|
||||||
turnStart: () => number
|
|
||||||
currentMessageId: () => string | undefined
|
currentMessageId: () => string | undefined
|
||||||
pendingMessage: () => string | undefined
|
pendingMessage: () => string | undefined
|
||||||
setPendingMessage: (value: string | undefined) => void
|
setPendingMessage: (value: string | undefined) => void
|
||||||
setActiveMessage: (message: UserMessage | undefined) => void
|
setActiveMessage: (message: UserMessage | undefined) => void
|
||||||
setTurnStart: (value: number) => void
|
|
||||||
autoScroll: { pause: () => void; forceScrollToBottom: () => void }
|
autoScroll: { pause: () => void; forceScrollToBottom: () => void }
|
||||||
scroller: () => HTMLDivElement | undefined
|
scroller: () => HTMLDivElement | undefined
|
||||||
anchor: (id: string) => string
|
anchor: (id: string) => string
|
||||||
|
revealMessage?: (id: string) => void
|
||||||
scheduleScrollState: (el: HTMLDivElement) => void
|
scheduleScrollState: (el: HTMLDivElement) => void
|
||||||
consumePendingMessage: (key: string) => string | undefined
|
consumePendingMessage: (key: string) => string | undefined
|
||||||
}) => {
|
}) => {
|
||||||
const visibleUserMessages = createMemo(() => input.visibleUserMessages())
|
const visibleUserMessages = createMemo(() => input.visibleUserMessages())
|
||||||
const messageById = createMemo(() => new Map(visibleUserMessages().map((m) => [m.id, m])))
|
const messageById = createMemo(() => new Map(visibleUserMessages().map((m) => [m.id, m])))
|
||||||
const messageIndex = createMemo(() => new Map(visibleUserMessages().map((m, i) => [m.id, i])))
|
|
||||||
let pendingKey = ""
|
let pendingKey = ""
|
||||||
let clearing = false
|
let clearing = false
|
||||||
|
|
||||||
@@ -77,6 +75,7 @@ export const useSessionHashScroll = (input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seek = (id: string, behavior: ScrollBehavior, left = 4): boolean => {
|
const seek = (id: string, behavior: ScrollBehavior, left = 4): boolean => {
|
||||||
|
input.revealMessage?.(id)
|
||||||
const el = document.getElementById(input.anchor(id))
|
const el = document.getElementById(input.anchor(id))
|
||||||
if (el) return scrollToElement(el, behavior)
|
if (el) return scrollToElement(el, behavior)
|
||||||
if (left <= 0) return false
|
if (left <= 0) return false
|
||||||
@@ -89,18 +88,7 @@ export const useSessionHashScroll = (input: {
|
|||||||
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
const scrollToMessage = (message: UserMessage, behavior: ScrollBehavior = "smooth") => {
|
||||||
cancel()
|
cancel()
|
||||||
if (input.currentMessageId() !== message.id) input.setActiveMessage(message)
|
if (input.currentMessageId() !== message.id) input.setActiveMessage(message)
|
||||||
|
input.revealMessage?.(message.id)
|
||||||
const index = messageIndex().get(message.id) ?? -1
|
|
||||||
if (index !== -1 && index < input.turnStart()) {
|
|
||||||
input.setTurnStart(index)
|
|
||||||
|
|
||||||
queue(() => {
|
|
||||||
seek(message.id, behavior)
|
|
||||||
})
|
|
||||||
|
|
||||||
updateHash(message.id)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seek(message.id, behavior)) {
|
if (seek(message.id, behavior)) {
|
||||||
updateHash(message.id)
|
updateHash(message.id)
|
||||||
@@ -154,7 +142,6 @@ export const useSessionHashScroll = (input: {
|
|||||||
if (!input.sessionID() || !input.messagesReady()) return
|
if (!input.sessionID() || !input.messagesReady()) return
|
||||||
|
|
||||||
visibleUserMessages()
|
visibleUserMessages()
|
||||||
input.turnStart()
|
|
||||||
|
|
||||||
let targetId = input.pendingMessage()
|
let targetId = input.pendingMessage()
|
||||||
if (!targetId) {
|
if (!targetId) {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
"./package.json": "./package.json",
|
"./package.json": "./package.json",
|
||||||
"./*": "./src/components/*.tsx",
|
"./*": "./src/components/*.tsx",
|
||||||
|
"./session-diff": "./src/components/session-diff.ts",
|
||||||
"./i18n/*": "./src/i18n/*.ts",
|
"./i18n/*": "./src/i18n/*.ts",
|
||||||
"./pierre": "./src/pierre/index.ts",
|
"./pierre": "./src/pierre/index.ts",
|
||||||
"./pierre/*": "./src/pierre/*.ts",
|
"./pierre/*": "./src/pierre/*.ts",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createEffect, For, Match, on, onCleanup, Show, Switch, type JSX } from "solid-js"
|
import { createEffect, For, Match, on, onCleanup, onMount, Show, Switch, type JSX } from "solid-js"
|
||||||
import { animate, type AnimationPlaybackControls } from "motion"
|
import { animate, type AnimationPlaybackControls } from "motion"
|
||||||
import { useI18n } from "../context/i18n"
|
import { useI18n } from "../context/i18n"
|
||||||
import { createStore } from "solid-js/store"
|
import { createStore } from "solid-js/store"
|
||||||
@@ -40,26 +40,76 @@ export interface BasicToolProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 }
|
const SPRING = { type: "spring" as const, visualDuration: 0.35, bounce: 0 }
|
||||||
|
const deferredMounts: Array<{ active: boolean; fn: () => void }> = []
|
||||||
|
let deferredFrame: number | undefined
|
||||||
|
|
||||||
|
function flushDeferredMounts() {
|
||||||
|
while (deferredMounts.length > 0) {
|
||||||
|
// Timeline tools are mounted top-to-bottom, but the viewport starts at the latest turn.
|
||||||
|
// Pop from the end so heavy default-open bodies near the bottom become interactive first.
|
||||||
|
const item = deferredMounts.pop()!
|
||||||
|
if (item.active) {
|
||||||
|
deferredFrame = deferredMounts.length > 0 ? requestAnimationFrame(flushDeferredMounts) : undefined
|
||||||
|
item.fn()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deferredFrame = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleDeferredFlush() {
|
||||||
|
if (deferredFrame !== undefined) return
|
||||||
|
deferredFrame = requestAnimationFrame(() => {
|
||||||
|
deferredFrame = requestAnimationFrame(flushDeferredMounts)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleDeferredMount(fn: () => void) {
|
||||||
|
const item = { active: true, fn }
|
||||||
|
deferredMounts.push(item)
|
||||||
|
scheduleDeferredFlush()
|
||||||
|
return () => {
|
||||||
|
item.active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleFrameMount(fn: () => void) {
|
||||||
|
const frame = requestAnimationFrame(fn)
|
||||||
|
return () => cancelAnimationFrame(frame)
|
||||||
|
}
|
||||||
|
|
||||||
export function BasicTool(props: BasicToolProps) {
|
export function BasicTool(props: BasicToolProps) {
|
||||||
const [state, setState] = createStore({
|
const [state, setState] = createStore({
|
||||||
open: props.defaultOpen ?? false,
|
open: props.defaultOpen ?? false,
|
||||||
ready: props.defaultOpen ?? false,
|
ready: !props.defer && (props.defaultOpen ?? false),
|
||||||
})
|
})
|
||||||
const open = () => state.open
|
const open = () => state.open
|
||||||
const ready = () => state.ready
|
const ready = () => state.ready
|
||||||
const pending = () => props.status === "pending" || props.status === "running"
|
const pending = () => props.status === "pending" || props.status === "running"
|
||||||
|
const hasChildren = () => (props.defer ? "children" in props : props.children)
|
||||||
|
|
||||||
let frame: number | undefined
|
let cancelReady: (() => void) | undefined
|
||||||
|
|
||||||
const cancel = () => {
|
const cancel = () => {
|
||||||
if (frame === undefined) return
|
cancelReady?.()
|
||||||
cancelAnimationFrame(frame)
|
cancelReady = undefined
|
||||||
frame = undefined
|
}
|
||||||
|
|
||||||
|
const scheduleReady = (initial = false) => {
|
||||||
|
cancel()
|
||||||
|
cancelReady = (initial ? scheduleDeferredMount : scheduleFrameMount)(() => {
|
||||||
|
cancelReady = undefined
|
||||||
|
if (!open()) return
|
||||||
|
setState("ready", true)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onCleanup(cancel)
|
onCleanup(cancel)
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (props.defer && open()) scheduleReady(true)
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (props.forceOpen) setState("open", true)
|
if (props.forceOpen) setState("open", true)
|
||||||
})
|
})
|
||||||
@@ -75,12 +125,7 @@ export function BasicTool(props: BasicToolProps) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel()
|
scheduleReady()
|
||||||
frame = requestAnimationFrame(() => {
|
|
||||||
frame = undefined
|
|
||||||
if (!open()) return
|
|
||||||
setState("ready", true)
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
{ defer: true },
|
{ defer: true },
|
||||||
),
|
),
|
||||||
@@ -189,7 +234,7 @@ export function BasicTool(props: BasicToolProps) {
|
|||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Show when={props.children && !props.hideDetails && !props.locked && !pending()}>
|
<Show when={hasChildren() && !props.hideDetails && !props.locked && !pending()}>
|
||||||
<Collapsible.Arrow />
|
<Collapsible.Arrow />
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
@@ -219,7 +264,7 @@ export function BasicTool(props: BasicToolProps) {
|
|||||||
</Collapsible.Trigger>
|
</Collapsible.Trigger>
|
||||||
)}
|
)}
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={props.animated && props.children && !props.hideDetails}>
|
<Show when={props.animated && hasChildren() && !props.hideDetails}>
|
||||||
<div
|
<div
|
||||||
ref={contentRef}
|
ref={contentRef}
|
||||||
data-slot="collapsible-content"
|
data-slot="collapsible-content"
|
||||||
@@ -229,10 +274,10 @@ export function BasicTool(props: BasicToolProps) {
|
|||||||
overflow: initialOpen ? "visible" : "hidden",
|
overflow: initialOpen ? "visible" : "hidden",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
<Show when={!props.defer || ready()}>{props.children}</Show>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
<Show when={!props.animated && props.children && !props.hideDetails}>
|
<Show when={!props.animated && hasChildren() && !props.hideDetails}>
|
||||||
<Collapsible.Content>
|
<Collapsible.Content>
|
||||||
<Show when={!props.defer || ready()}>{props.children}</Show>
|
<Show when={!props.defer || ready()}>{props.children}</Show>
|
||||||
</Collapsible.Content>
|
</Collapsible.Content>
|
||||||
|
|||||||
@@ -150,6 +150,7 @@ export interface MessagePartProps {
|
|||||||
message: MessageType
|
message: MessageType
|
||||||
hideDetails?: boolean
|
hideDetails?: boolean
|
||||||
defaultOpen?: boolean
|
defaultOpen?: boolean
|
||||||
|
deferToolContent?: boolean
|
||||||
showAssistantCopyPartID?: string | null
|
showAssistantCopyPartID?: string | null
|
||||||
turnDurationMs?: number
|
turnDurationMs?: number
|
||||||
}
|
}
|
||||||
@@ -486,12 +487,12 @@ function same<T>(a: readonly T[] | undefined, b: readonly T[] | undefined) {
|
|||||||
return a.every((x, i) => x === b[i])
|
return a.every((x, i) => x === b[i])
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartRef = {
|
export type PartRef = {
|
||||||
messageID: string
|
messageID: string
|
||||||
partID: string
|
partID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PartGroup =
|
export type PartGroup =
|
||||||
| {
|
| {
|
||||||
key: string
|
key: string
|
||||||
type: "part"
|
type: "part"
|
||||||
@@ -520,14 +521,14 @@ function sameGroup(a: PartGroup, b: PartGroup) {
|
|||||||
return a.refs.every((ref, i) => sameRef(ref, b.refs[i]!))
|
return a.refs.every((ref, i) => sameRef(ref, b.refs[i]!))
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameGroups(a: readonly PartGroup[] | undefined, b: readonly PartGroup[] | undefined) {
|
export function sameGroups(a: readonly PartGroup[] | undefined, b: readonly PartGroup[] | undefined) {
|
||||||
if (a === b) return true
|
if (a === b) return true
|
||||||
if (!a || !b) return false
|
if (!a || !b) return false
|
||||||
if (a.length !== b.length) return false
|
if (a.length !== b.length) return false
|
||||||
return a.every((item, i) => sameGroup(item, b[i]!))
|
return a.every((item, i) => sameGroup(item, b[i]!))
|
||||||
}
|
}
|
||||||
|
|
||||||
function groupParts(parts: { messageID: string; part: PartType }[]) {
|
export function groupParts(parts: { messageID: string; part: PartType }[]) {
|
||||||
const result: PartGroup[] = []
|
const result: PartGroup[] = []
|
||||||
let start = -1
|
let start = -1
|
||||||
|
|
||||||
@@ -575,7 +576,7 @@ function index<T extends { id: string }>(items: readonly T[]) {
|
|||||||
return new Map(items.map((item) => [item.id, item] as const))
|
return new Map(items.map((item) => [item.id, item] as const))
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderable(part: PartType, showReasoningSummaries = true) {
|
export function renderable(part: PartType, showReasoningSummaries = true) {
|
||||||
if (part.type === "tool") {
|
if (part.type === "tool") {
|
||||||
if (HIDDEN_TOOLS.has(part.tool)) return false
|
if (HIDDEN_TOOLS.has(part.tool)) return false
|
||||||
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
|
if (part.tool === "question") return part.state.status !== "pending" && part.state.status !== "running"
|
||||||
@@ -591,7 +592,7 @@ function toolDefaultOpen(tool: string, shell = false, edit = false) {
|
|||||||
if (tool === "edit" || tool === "write" || tool === "apply_patch") return edit
|
if (tool === "edit" || tool === "write" || tool === "apply_patch") return edit
|
||||||
}
|
}
|
||||||
|
|
||||||
function partDefaultOpen(part: PartType, shell = false, edit = false) {
|
export function partDefaultOpen(part: PartType, shell = false, edit = false) {
|
||||||
if (part.type !== "tool") return
|
if (part.type !== "tool") return
|
||||||
return toolDefaultOpen(part.tool, shell, edit)
|
return toolDefaultOpen(part.tool, shell, edit)
|
||||||
}
|
}
|
||||||
@@ -904,7 +905,7 @@ export function AssistantMessageDisplay(props: {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
export function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
||||||
const i18n = useI18n()
|
const i18n = useI18n()
|
||||||
const [open, setOpen] = createSignal(false)
|
const [open, setOpen] = createSignal(false)
|
||||||
const pending = createMemo(
|
const pending = createMemo(
|
||||||
@@ -914,7 +915,13 @@ function ContextToolGroup(props: { parts: ToolPart[]; busy?: boolean }) {
|
|||||||
const summary = createMemo(() => contextToolSummary(props.parts))
|
const summary = createMemo(() => contextToolSummary(props.parts))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible open={open()} onOpenChange={setOpen} variant="ghost" class="tool-collapsible">
|
<Collapsible
|
||||||
|
open={open()}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
variant="ghost"
|
||||||
|
class="tool-collapsible"
|
||||||
|
data-timeline-part-ids={props.parts.map((part) => part.id).join(",")}
|
||||||
|
>
|
||||||
<Collapsible.Trigger>
|
<Collapsible.Trigger>
|
||||||
<div data-component="context-tool-group-trigger">
|
<div data-component="context-tool-group-trigger">
|
||||||
<span
|
<span
|
||||||
@@ -1077,7 +1084,7 @@ export function UserMessageDisplay(props: { message: UserMessage; parts: PartTyp
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-component="user-message">
|
<div data-component="user-message" data-timeline-part-id={textPart()?.id}>
|
||||||
<Show when={attachments().length > 0}>
|
<Show when={attachments().length > 0}>
|
||||||
<div data-slot="user-message-attachments">
|
<div data-slot="user-message-attachments">
|
||||||
<For each={attachments()}>
|
<For each={attachments()}>
|
||||||
@@ -1228,6 +1235,7 @@ export function Part(props: MessagePartProps) {
|
|||||||
message={props.message}
|
message={props.message}
|
||||||
hideDetails={props.hideDetails}
|
hideDetails={props.hideDetails}
|
||||||
defaultOpen={props.defaultOpen}
|
defaultOpen={props.defaultOpen}
|
||||||
|
deferToolContent={props.deferToolContent}
|
||||||
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
showAssistantCopyPartID={props.showAssistantCopyPartID}
|
||||||
turnDurationMs={props.turnDurationMs}
|
turnDurationMs={props.turnDurationMs}
|
||||||
/>
|
/>
|
||||||
@@ -1244,6 +1252,7 @@ export interface ToolProps {
|
|||||||
status?: string
|
status?: string
|
||||||
hideDetails?: boolean
|
hideDetails?: boolean
|
||||||
defaultOpen?: boolean
|
defaultOpen?: boolean
|
||||||
|
deferContent?: boolean
|
||||||
forceOpen?: boolean
|
forceOpen?: boolean
|
||||||
locked?: boolean
|
locked?: boolean
|
||||||
}
|
}
|
||||||
@@ -1344,7 +1353,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={!hideQuestion()}>
|
<Show when={!hideQuestion()}>
|
||||||
<div data-component="tool-part-wrapper">
|
<div data-component="tool-part-wrapper" data-timeline-part-id={part().id}>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={part().state.status === "error" && (part().state as any).error}>
|
<Match when={part().state.status === "error" && (part().state as any).error}>
|
||||||
{(error) => {
|
{(error) => {
|
||||||
@@ -1382,6 +1391,7 @@ PART_MAPPING["tool"] = function ToolPartDisplay(props) {
|
|||||||
status={part().state.status}
|
status={part().state.status}
|
||||||
hideDetails={props.hideDetails}
|
hideDetails={props.hideDetails}
|
||||||
defaultOpen={props.defaultOpen}
|
defaultOpen={props.defaultOpen}
|
||||||
|
deferContent={props.deferToolContent}
|
||||||
/>
|
/>
|
||||||
</Match>
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
@@ -1487,7 +1497,7 @@ PART_MAPPING["text"] = function TextPartDisplay(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={text()}>
|
<Show when={text()}>
|
||||||
<div data-component="text-part">
|
<div data-component="text-part" data-timeline-part-id={part().id}>
|
||||||
<div data-slot="text-part-body">
|
<div data-slot="text-part-body">
|
||||||
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
|
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
|
||||||
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
|
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
|
||||||
@@ -1531,7 +1541,7 @@ PART_MAPPING["reasoning"] = function ReasoningPartDisplay(props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Show when={text()}>
|
<Show when={text()}>
|
||||||
<div data-component="reasoning-part">
|
<div data-component="reasoning-part" data-timeline-part-id={part().id}>
|
||||||
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
|
<Show when={streaming()} fallback={<Markdown text={text()} cacheKey={part().id} streaming={false} />}>
|
||||||
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
|
<PacedMarkdown text={text()} cacheKey={part().id} streaming={streaming()} />
|
||||||
</Show>
|
</Show>
|
||||||
@@ -1909,7 +1919,7 @@ ToolRegistry.register({
|
|||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="code-lines"
|
icon="code-lines"
|
||||||
defer
|
defer={props.deferContent !== false}
|
||||||
trigger={
|
trigger={
|
||||||
<div data-component="edit-trigger">
|
<div data-component="edit-trigger">
|
||||||
<div data-slot="message-part-title-area">
|
<div data-slot="message-part-title-area">
|
||||||
@@ -1970,7 +1980,7 @@ ToolRegistry.register({
|
|||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="code-lines"
|
icon="code-lines"
|
||||||
defer
|
defer={props.deferContent !== false}
|
||||||
trigger={
|
trigger={
|
||||||
<div data-component="write-trigger">
|
<div data-component="write-trigger">
|
||||||
<div data-slot="message-part-title-area">
|
<div data-slot="message-part-title-area">
|
||||||
@@ -2052,7 +2062,7 @@ ToolRegistry.register({
|
|||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="code-lines"
|
icon="code-lines"
|
||||||
defer
|
defer={props.deferContent !== false}
|
||||||
trigger={{
|
trigger={{
|
||||||
title: i18n.t("ui.tool.patch"),
|
title: i18n.t("ui.tool.patch"),
|
||||||
subtitle: subtitle(),
|
subtitle: subtitle(),
|
||||||
@@ -2124,7 +2134,7 @@ ToolRegistry.register({
|
|||||||
</Accordion.Trigger>
|
</Accordion.Trigger>
|
||||||
</StickyAccordionHeader>
|
</StickyAccordionHeader>
|
||||||
<Accordion.Content>
|
<Accordion.Content>
|
||||||
<Show when={visible()}>
|
<Show when={props.deferContent === false || visible()}>
|
||||||
<div data-component="apply-patch-file-diff">
|
<div data-component="apply-patch-file-diff">
|
||||||
<Dynamic
|
<Dynamic
|
||||||
component={fileComponent}
|
component={fileComponent}
|
||||||
@@ -2149,7 +2159,7 @@ ToolRegistry.register({
|
|||||||
<BasicTool
|
<BasicTool
|
||||||
{...props}
|
{...props}
|
||||||
icon="code-lines"
|
icon="code-lines"
|
||||||
defer
|
defer={props.deferContent !== false}
|
||||||
trigger={
|
trigger={
|
||||||
<div data-component="edit-trigger">
|
<div data-component="edit-trigger">
|
||||||
<div data-slot="message-part-title-area">
|
<div data-slot="message-part-title-area">
|
||||||
|
|||||||
Reference in New Issue
Block a user