Explorar el Código

feat(tui): preserve scroll position on submit

Kit Langton hace 1 semana
padre
commit
6cc7ccd3c1
Se han modificado 1 ficheros con 78 adiciones y 43 borrados
  1. 78 43
      packages/tui/src/routes/session/index.tsx

+ 78 - 43
packages/tui/src/routes/session/index.tsx

@@ -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>