diff options
| author | Adam <[email protected]> | 2026-03-04 07:18:03 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2026-03-04 07:18:09 -0600 |
| commit | 2ccf21de991e996a3cb1006b513a573a90ddc4dd (patch) | |
| tree | 04df3f0c5bfd0875e3b5473d962186841eaa924f /packages | |
| parent | d7569a5625e4a2287195c27a84330af1d9d2c3df (diff) | |
| download | opencode-2ccf21de991e996a3cb1006b513a573a90ddc4dd.tar.gz opencode-2ccf21de991e996a3cb1006b513a573a90ddc4dd.zip | |
fix(app): loading session should be scrolled to the bottom
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/app/src/pages/session/message-timeline.tsx | 419 |
1 files changed, 210 insertions, 209 deletions
diff --git a/packages/app/src/pages/session/message-timeline.tsx b/packages/app/src/pages/session/message-timeline.tsx index a7f503d5a..433c36e2e 100644 --- a/packages/app/src/pages/session/message-timeline.tsx +++ b/packages/app/src/pages/session/message-timeline.tsx @@ -550,227 +550,228 @@ export function MessageTimeline(props: { "--sticky-accordion-top": showHeader() ? "48px" : "0px", }} > - <Show when={showHeader()}> - <div - data-session-title - classList={{ - "sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true, - "w-full": true, - "pb-4": true, - "pl-2 pr-3 md:pl-4 md:pr-3": true, - "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered, - }} - > - <div class="h-12 w-full flex items-center justify-between gap-2"> - <div class="flex items-center gap-1 min-w-0 flex-1 pr-3"> - <Show when={parentID()}> - <IconButton - tabIndex={-1} - icon="arrow-left" - variant="ghost" - onClick={navigateParent} - aria-label={language.t("common.goBack")} - /> - </Show> - <Show when={titleValue() || title.editing}> - <Show - when={title.editing} - fallback={ - <h1 - class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2" - onDblClick={openTitleEditor} - > - {titleValue()} - </h1> - } - > - <InlineInput - ref={(el) => { - titleRef = el - }} - value={title.draft} - disabled={title.saving} - class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]" - style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }} - onInput={(event) => setTitle("draft", event.currentTarget.value)} - onKeyDown={(event) => { - event.stopPropagation() - if (event.key === "Enter") { - event.preventDefault() - void saveTitleEditor() - return - } - if (event.key === "Escape") { - event.preventDefault() - closeTitleEditor() - } - }} - onBlur={closeTitleEditor} + <div ref={props.setContentRef} class="min-w-0 w-full"> + <Show when={showHeader()}> + <div + data-session-title + classList={{ + "sticky top-0 z-30 bg-[linear-gradient(to_bottom,var(--background-stronger)_48px,transparent)]": true, + "w-full": true, + "pb-4": true, + "pl-2 pr-3 md:pl-4 md:pr-3": true, + "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered, + }} + > + <div class="h-12 w-full flex items-center justify-between gap-2"> + <div class="flex items-center gap-1 min-w-0 flex-1 pr-3"> + <Show when={parentID()}> + <IconButton + tabIndex={-1} + icon="arrow-left" + variant="ghost" + onClick={navigateParent} + aria-label={language.t("common.goBack")} /> </Show> - </Show> - </div> - <Show when={sessionID()}> - {(id) => ( - <div class="shrink-0 flex items-center gap-3"> - <SessionContextUsage placement="bottom" /> - <DropdownMenu - gutter={4} - placement="bottom-end" - open={title.menuOpen} - onOpenChange={(open) => setTitle("menuOpen", open)} + <Show when={titleValue() || title.editing}> + <Show + when={title.editing} + fallback={ + <h1 + class="text-14-medium text-text-strong truncate grow-1 min-w-0 pl-2" + onDblClick={openTitleEditor} + > + {titleValue()} + </h1> + } > - <DropdownMenu.Trigger - as={IconButton} - icon="dot-grid" - variant="ghost" - class="size-6 rounded-md data-[expanded]:bg-surface-base-active" - aria-label={language.t("common.moreOptions")} - /> - <DropdownMenu.Portal> - <DropdownMenu.Content - style={{ "min-width": "104px" }} - onCloseAutoFocus={(event) => { - if (!title.pendingRename) return + <InlineInput + ref={(el) => { + titleRef = el + }} + value={title.draft} + disabled={title.saving} + class="text-14-medium text-text-strong grow-1 min-w-0 pl-2 rounded-[6px]" + style={{ "--inline-input-shadow": "var(--shadow-xs-border-select)" }} + onInput={(event) => setTitle("draft", event.currentTarget.value)} + onKeyDown={(event) => { + event.stopPropagation() + if (event.key === "Enter") { event.preventDefault() - setTitle("pendingRename", false) - openTitleEditor() - }} - > - <DropdownMenu.Item - onSelect={() => { - setTitle("pendingRename", true) - setTitle("menuOpen", false) + void saveTitleEditor() + return + } + if (event.key === "Escape") { + event.preventDefault() + closeTitleEditor() + } + }} + onBlur={closeTitleEditor} + /> + </Show> + </Show> + </div> + <Show when={sessionID()}> + {(id) => ( + <div class="shrink-0 flex items-center gap-3"> + <SessionContextUsage placement="bottom" /> + <DropdownMenu + gutter={4} + placement="bottom-end" + open={title.menuOpen} + onOpenChange={(open) => setTitle("menuOpen", open)} + > + <DropdownMenu.Trigger + as={IconButton} + icon="dot-grid" + variant="ghost" + class="size-6 rounded-md data-[expanded]:bg-surface-base-active" + aria-label={language.t("common.moreOptions")} + /> + <DropdownMenu.Portal> + <DropdownMenu.Content + style={{ "min-width": "104px" }} + onCloseAutoFocus={(event) => { + if (!title.pendingRename) return + event.preventDefault() + setTitle("pendingRename", false) + openTitleEditor() }} > - <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel> - </DropdownMenu.Item> - <DropdownMenu.Item onSelect={() => void archiveSession(id())}> - <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel> - </DropdownMenu.Item> - <DropdownMenu.Separator /> - <DropdownMenu.Item - onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)} - > - <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel> - </DropdownMenu.Item> - </DropdownMenu.Content> - </DropdownMenu.Portal> - </DropdownMenu> - </div> - )} - </Show> - </div> - </div> - </Show> - - <div - ref={props.setContentRef} - role="log" - class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]" - classList={{ - "w-full": true, - "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered, - "mt-0.5": props.centered, - "mt-0": !props.centered, - }} - > - <Show when={props.turnStart > 0 || props.historyMore}> - <div class="w-full flex justify-center"> - <Button - variant="ghost" - size="large" - class="text-12-medium opacity-50" - disabled={props.historyLoading} - onClick={props.onLoadEarlier} - > - {props.historyLoading - ? language.t("session.messages.loadingEarlier") - : language.t("session.messages.loadEarlier")} - </Button> + <DropdownMenu.Item + onSelect={() => { + setTitle("pendingRename", true) + setTitle("menuOpen", false) + }} + > + <DropdownMenu.ItemLabel>{language.t("common.rename")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Item onSelect={() => void archiveSession(id())}> + <DropdownMenu.ItemLabel>{language.t("common.archive")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + <DropdownMenu.Separator /> + <DropdownMenu.Item + onSelect={() => dialog.show(() => <DialogDeleteSession sessionID={id()} />)} + > + <DropdownMenu.ItemLabel>{language.t("common.delete")}</DropdownMenu.ItemLabel> + </DropdownMenu.Item> + </DropdownMenu.Content> + </DropdownMenu.Portal> + </DropdownMenu> + </div> + )} + </Show> + </div> </div> </Show> - <For each={rendered()}> - {(messageID) => { - const active = createMemo(() => activeMessageID() === messageID) - const queued = createMemo(() => { - if (active()) return false - const activeID = activeMessageID() - if (activeID) return messageID > activeID - return false - }) - const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], { - equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), - }) - const commentCount = createMemo(() => comments().length) - return ( - <div - id={props.anchor(messageID)} - data-message-id={messageID} - ref={(el) => { - props.onRegisterMessage(el, messageID) - onCleanup(() => props.onUnregisterMessage(messageID)) - }} - classList={{ - "min-w-0 w-full max-w-full": true, - "md:max-w-200 2xl:max-w-[1000px]": props.centered, - }} + + <div + role="log" + class="flex flex-col gap-12 items-start justify-start pb-16 transition-[margin]" + classList={{ + "w-full": true, + "md:max-w-200 md:mx-auto 2xl:max-w-[1000px]": props.centered, + "mt-0.5": props.centered, + "mt-0": !props.centered, + }} + > + <Show when={props.turnStart > 0 || props.historyMore}> + <div class="w-full flex justify-center"> + <Button + variant="ghost" + size="large" + class="text-12-medium opacity-50" + disabled={props.historyLoading} + onClick={props.onLoadEarlier} > - <Show when={commentCount() > 0}> - <div class="w-full px-4 md:px-5 pb-2"> - <div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar"> - <div class="flex w-max min-w-full justify-end gap-2"> - <Index each={comments()}> - {(commentAccessor: () => MessageComment) => { - const comment = createMemo(() => commentAccessor()) - return ( - <div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2"> - <div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong"> - <FileIcon - node={{ path: comment().path, type: "file" }} - class="size-3.5 shrink-0" - /> - <span class="truncate">{getFilename(comment().path)}</span> - <Show when={comment().selection}> - {(selection) => ( - <span class="shrink-0 text-text-weak"> - {selection().startLine === selection().endLine - ? `:${selection().startLine}` - : `:${selection().startLine}-${selection().endLine}`} - </span> - )} - </Show> - </div> - <div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words"> - {comment().comment} + {props.historyLoading + ? language.t("session.messages.loadingEarlier") + : language.t("session.messages.loadEarlier")} + </Button> + </div> + </Show> + <For each={rendered()}> + {(messageID) => { + const active = createMemo(() => activeMessageID() === messageID) + const queued = createMemo(() => { + if (active()) return false + const activeID = activeMessageID() + if (activeID) return messageID > activeID + return false + }) + const comments = createMemo(() => messageComments(sync.data.part[messageID] ?? []), [], { + equals: (a, b) => JSON.stringify(a) === JSON.stringify(b), + }) + const commentCount = createMemo(() => comments().length) + return ( + <div + id={props.anchor(messageID)} + data-message-id={messageID} + ref={(el) => { + props.onRegisterMessage(el, messageID) + onCleanup(() => props.onUnregisterMessage(messageID)) + }} + classList={{ + "min-w-0 w-full max-w-full": true, + "md:max-w-200 2xl:max-w-[1000px]": props.centered, + }} + > + <Show when={commentCount() > 0}> + <div class="w-full px-4 md:px-5 pb-2"> + <div class="ml-auto max-w-[82%] overflow-x-auto no-scrollbar"> + <div class="flex w-max min-w-full justify-end gap-2"> + <Index each={comments()}> + {(commentAccessor: () => MessageComment) => { + const comment = createMemo(() => commentAccessor()) + return ( + <div class="shrink-0 max-w-[260px] rounded-[6px] border border-border-weak-base bg-background-stronger px-2.5 py-2"> + <div class="flex items-center gap-1.5 min-w-0 text-11-medium text-text-strong"> + <FileIcon + node={{ path: comment().path, type: "file" }} + class="size-3.5 shrink-0" + /> + <span class="truncate">{getFilename(comment().path)}</span> + <Show when={comment().selection}> + {(selection) => ( + <span class="shrink-0 text-text-weak"> + {selection().startLine === selection().endLine + ? `:${selection().startLine}` + : `:${selection().startLine}-${selection().endLine}`} + </span> + )} + </Show> + </div> + <div class="pt-1 text-12-regular text-text-strong whitespace-pre-wrap break-words"> + {comment().comment} + </div> </div> - </div> - ) - }} - </Index> + ) + }} + </Index> + </div> </div> </div> - </div> - </Show> - <SessionTurn - sessionID={sessionID() ?? ""} - messageID={messageID} - active={active()} - queued={queued()} - status={active() ? sessionStatus() : undefined} - showReasoningSummaries={settings.general.showReasoningSummaries()} - shellToolDefaultOpen={settings.general.shellToolPartsExpanded()} - editToolDefaultOpen={settings.general.editToolPartsExpanded()} - classes={{ - root: "min-w-0 w-full relative", - content: "flex flex-col justify-between !overflow-visible", - container: "w-full px-4 md:px-5", - }} - /> - </div> - ) - }} - </For> + </Show> + <SessionTurn + sessionID={sessionID() ?? ""} + messageID={messageID} + active={active()} + queued={queued()} + status={active() ? sessionStatus() : undefined} + showReasoningSummaries={settings.general.showReasoningSummaries()} + shellToolDefaultOpen={settings.general.shellToolPartsExpanded()} + editToolDefaultOpen={settings.general.editToolPartsExpanded()} + classes={{ + root: "min-w-0 w-full relative", + content: "flex flex-col justify-between !overflow-visible", + container: "w-full px-4 md:px-5", + }} + /> + </div> + ) + }} + </For> + </div> </div> </ScrollView> </div> |
