|
|
@@ -295,6 +295,7 @@ export function Session() {
|
|
|
let seeded = false
|
|
|
let sent = false
|
|
|
let scroll: ScrollBoxRenderable
|
|
|
+ const [showJumpToBottom, setShowJumpToBottom] = createSignal(false)
|
|
|
const [prompt, setPrompt] = createSignal<PromptRef>()
|
|
|
const bind = (r: PromptRef | undefined) => {
|
|
|
setPrompt(r)
|
|
|
@@ -314,6 +315,11 @@ export function Session() {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+ const updateJumpToBottom = () => {
|
|
|
+ if (!scroll || scroll.isDestroyed) return
|
|
|
+ setShowJumpToBottom(scroll.scrollTop + scroll.viewport.height < scroll.scrollHeight - 1)
|
|
|
+ }
|
|
|
+
|
|
|
// Tail-first transcript mounting: only the newest rows mount when the session opens, and the
|
|
|
// rest backfill in chunks shortly after, so switching to a long session costs the visible tail
|
|
|
// instead of the whole transcript. Until backfill pins the count, the hidden span derives from
|
|
|
@@ -322,6 +328,10 @@ export function Session() {
|
|
|
const [hiddenRows, setHiddenRows] = createSignal<number>()
|
|
|
const hidden = createMemo(() => Math.max(0, Math.min(hiddenRows() ?? Infinity, rows.length - TRANSCRIPT_TAIL_ROWS)))
|
|
|
const visibleRows = createMemo(() => (hidden() === 0 ? rows : rows.slice(hidden())))
|
|
|
+ createEffect(() => {
|
|
|
+ visibleRows().length
|
|
|
+ afterLayout(updateJumpToBottom)
|
|
|
+ })
|
|
|
createEffect(() => {
|
|
|
const current = hidden()
|
|
|
if (current === 0) return
|
|
|
@@ -409,6 +419,7 @@ export function Session() {
|
|
|
setTimeout(() => {
|
|
|
if (!scroll || scroll.isDestroyed) return
|
|
|
scroll.scrollTo(scroll.scrollHeight)
|
|
|
+ updateJumpToBottom()
|
|
|
}, 50)
|
|
|
}
|
|
|
|
|
|
@@ -421,6 +432,7 @@ export function Session() {
|
|
|
run: () => {
|
|
|
clearMessageNavigation()
|
|
|
scroll.scrollBy(-scroll.height / 2)
|
|
|
+ updateJumpToBottom()
|
|
|
dialog.clear()
|
|
|
},
|
|
|
},
|
|
|
@@ -432,6 +444,7 @@ export function Session() {
|
|
|
run: () => {
|
|
|
clearMessageNavigation()
|
|
|
scroll.scrollBy(scroll.height / 2)
|
|
|
+ updateJumpToBottom()
|
|
|
dialog.clear()
|
|
|
},
|
|
|
},
|
|
|
@@ -443,6 +456,7 @@ export function Session() {
|
|
|
run: () => {
|
|
|
clearMessageNavigation()
|
|
|
scroll.scrollBy(-1)
|
|
|
+ updateJumpToBottom()
|
|
|
dialog.clear()
|
|
|
},
|
|
|
},
|
|
|
@@ -454,6 +468,7 @@ export function Session() {
|
|
|
run: () => {
|
|
|
clearMessageNavigation()
|
|
|
scroll.scrollBy(1)
|
|
|
+ updateJumpToBottom()
|
|
|
dialog.clear()
|
|
|
},
|
|
|
},
|
|
|
@@ -465,6 +480,7 @@ export function Session() {
|
|
|
run: () => {
|
|
|
clearMessageNavigation()
|
|
|
scroll.scrollBy(-scroll.height / 4)
|
|
|
+ updateJumpToBottom()
|
|
|
dialog.clear()
|
|
|
},
|
|
|
},
|
|
|
@@ -476,6 +492,7 @@ export function Session() {
|
|
|
run: () => {
|
|
|
clearMessageNavigation()
|
|
|
scroll.scrollBy(scroll.height / 4)
|
|
|
+ updateJumpToBottom()
|
|
|
dialog.clear()
|
|
|
},
|
|
|
},
|
|
|
@@ -490,6 +507,7 @@ export function Session() {
|
|
|
run: () => {
|
|
|
clearMessageNavigation()
|
|
|
scroll.scrollTo(0)
|
|
|
+ updateJumpToBottom()
|
|
|
dialog.clear()
|
|
|
},
|
|
|
},
|
|
|
@@ -501,6 +519,7 @@ export function Session() {
|
|
|
run: () => {
|
|
|
clearMessageNavigation()
|
|
|
scroll.scrollTo(scroll.scrollHeight)
|
|
|
+ updateJumpToBottom()
|
|
|
dialog.clear()
|
|
|
},
|
|
|
},
|
|
|
@@ -960,48 +979,67 @@ export function Session() {
|
|
|
<box flexDirection="row" flexGrow={1} minHeight={0}>
|
|
|
<box flexGrow={1} minHeight={0} paddingBottom={1} paddingLeft={2} paddingRight={2} gap={1}>
|
|
|
<Show when={session()}>
|
|
|
- <scrollbox
|
|
|
- ref={(r) => (scroll = r)}
|
|
|
- viewportOptions={{
|
|
|
- paddingRight: showScrollbar() ? 1 : 0,
|
|
|
- }}
|
|
|
- verticalScrollbarOptions={{
|
|
|
- paddingLeft: 1,
|
|
|
- visible: showScrollbar(),
|
|
|
- trackOptions: {
|
|
|
- backgroundColor: theme.raise(theme.background.surface.offset),
|
|
|
- foregroundColor: theme.border.default,
|
|
|
- },
|
|
|
- }}
|
|
|
- stickyScroll={!navigationMessage()}
|
|
|
- stickyStart="bottom"
|
|
|
- flexGrow={1}
|
|
|
- scrollAcceleration={scrollAcceleration()}
|
|
|
- >
|
|
|
- <For each={visibleRows()}>
|
|
|
- {(row, index) => (
|
|
|
- <SessionRowView
|
|
|
- row={row}
|
|
|
- message={(messageID) => data.session.message.get(route.sessionID, messageID)}
|
|
|
- boundaryID={boundaries()[index() + hidden()]}
|
|
|
+ <box flexGrow={1} minHeight={0}>
|
|
|
+ <scrollbox
|
|
|
+ id="session-transcript"
|
|
|
+ ref={(r) => (scroll = r)}
|
|
|
+ onMouseScroll={() => setTimeout(updateJumpToBottom, 0)}
|
|
|
+ viewportOptions={{
|
|
|
+ paddingRight: showScrollbar() ? 1 : 0,
|
|
|
+ }}
|
|
|
+ verticalScrollbarOptions={{
|
|
|
+ paddingLeft: 1,
|
|
|
+ visible: showScrollbar(),
|
|
|
+ trackOptions: {
|
|
|
+ backgroundColor: theme.raise(theme.background.surface.offset),
|
|
|
+ foregroundColor: theme.border.default,
|
|
|
+ },
|
|
|
+ }}
|
|
|
+ stickyScroll={!navigationMessage()}
|
|
|
+ stickyStart="bottom"
|
|
|
+ flexGrow={1}
|
|
|
+ scrollAcceleration={scrollAcceleration()}
|
|
|
+ >
|
|
|
+ <For each={visibleRows()}>
|
|
|
+ {(row, index) => (
|
|
|
+ <SessionRowView
|
|
|
+ row={row}
|
|
|
+ message={(messageID) => data.session.message.get(route.sessionID, messageID)}
|
|
|
+ boundaryID={boundaries()[index() + hidden()]}
|
|
|
+ />
|
|
|
+ )}
|
|
|
+ </For>
|
|
|
+ <BackgroundToolHint messages={messages()} />
|
|
|
+ <Show when={session()?.revert?.messageID}>
|
|
|
+ <RevertMessage
|
|
|
+ count={
|
|
|
+ messages().filter(
|
|
|
+ (message) => message.id >= session()!.revert!.messageID && message.type === "user",
|
|
|
+ ).length
|
|
|
+ }
|
|
|
+ files={session()!.revert!.files ?? []}
|
|
|
/>
|
|
|
- )}
|
|
|
- </For>
|
|
|
- <BackgroundToolHint messages={messages()} />
|
|
|
- <Show when={session()?.revert?.messageID}>
|
|
|
- <RevertMessage
|
|
|
- count={
|
|
|
- messages().filter(
|
|
|
- (message) => message.id >= session()!.revert!.messageID && message.type === "user",
|
|
|
- ).length
|
|
|
- }
|
|
|
- files={session()!.revert!.files ?? []}
|
|
|
- />
|
|
|
- </Show>
|
|
|
- <Show when={navigationSlack()}>
|
|
|
- {(height) => <box id={NAVIGATION_SLACK_ID} height={height()} flexShrink={0} />}
|
|
|
+ </Show>
|
|
|
+ <Show when={navigationSlack()}>
|
|
|
+ {(height) => <box id={NAVIGATION_SLACK_ID} height={height()} flexShrink={0} />}
|
|
|
+ </Show>
|
|
|
+ </scrollbox>
|
|
|
+ <Show when={showJumpToBottom()}>
|
|
|
+ <box
|
|
|
+ id="session-jump-to-bottom"
|
|
|
+ position="absolute"
|
|
|
+ zIndex={1000}
|
|
|
+ right={showScrollbar() ? 2 : 0}
|
|
|
+ bottom={0}
|
|
|
+ backgroundColor={theme.raise(theme.background.default)}
|
|
|
+ paddingLeft={1}
|
|
|
+ paddingRight={1}
|
|
|
+ onMouseUp={toBottom}
|
|
|
+ >
|
|
|
+ <text fg={theme.text.default}>↓ jump to bottom</text>
|
|
|
+ </box>
|
|
|
</Show>
|
|
|
- </scrollbox>
|
|
|
+ </box>
|
|
|
<box flexShrink={0}>
|
|
|
<Composer
|
|
|
sessionID={route.sessionID}
|
|
|
@@ -1034,9 +1072,6 @@ export function Session() {
|
|
|
visible={true}
|
|
|
ref={bind}
|
|
|
disabled={false}
|
|
|
- onSubmit={() => {
|
|
|
- toBottom()
|
|
|
- }}
|
|
|
sessionID={route.sessionID}
|
|
|
/>
|
|
|
</Match>
|