fix(app): less auto-expand/collapse

This commit is contained in:
Adam
2026-03-08 07:11:15 -05:00
parent f386137fba
commit c53d1d3ad8
3 changed files with 29 additions and 74 deletions
@@ -58,11 +58,9 @@ export function ContextToolGroupHeader(props: {
<ToolCall <ToolCall
variant="row" variant="row"
icon="magnifying-glass-menu" icon="magnifying-glass-menu"
open={!props.pending && props.open} open={props.open}
showArrow={!props.pending} showArrow
onOpenChange={(v) => { onOpenChange={props.onOpenChange}
if (!props.pending) props.onOpenChange(v)
}}
trigger={ trigger={
<div data-component="context-tool-group-trigger" data-pending={props.pending || undefined}> <div data-component="context-tool-group-trigger" data-pending={props.pending || undefined}>
<span <span
+15 -39
View File
@@ -1,4 +1,3 @@
import { usePageVisibility } from "@solid-primitives/page-visibility"
import { Component, createEffect, createMemo, createSignal, For, Match, on, Show, Switch, type JSX } from "solid-js" import { Component, createEffect, createMemo, createSignal, For, Match, on, Show, Switch, type JSX } from "solid-js"
import stripAnsi from "strip-ansi" import stripAnsi from "strip-ansi"
import { createStore } from "solid-js/store" import { createStore } from "solid-js/store"
@@ -39,7 +38,7 @@ import { TextShimmer } from "./text-shimmer"
import { list } from "./text-utils" import { list } from "./text-utils"
import { GrowBox } from "./grow-box" import { GrowBox } from "./grow-box"
import { COLLAPSIBLE_SPRING } from "./motion" import { COLLAPSIBLE_SPRING } from "./motion"
import { busy, hold, createThrottledValue, useToolFade, useContextToolPending } from "./tool-utils" import { busy, createThrottledValue, useToolFade, useContextToolPending } from "./tool-utils"
import { ContextToolGroupHeader, ContextToolExpandedList, ContextToolRollingResults } from "./context-tool-results" import { ContextToolGroupHeader, ContextToolExpandedList, ContextToolRollingResults } from "./context-tool-results"
import { ShellRollingResults } from "./shell-rolling-results" import { ShellRollingResults } from "./shell-rolling-results"
@@ -273,19 +272,6 @@ function createGroupOpenState() {
return { read, controlled, write } return { read, controlled, write }
} }
function shouldCollapseGroup(
statuses: (string | undefined)[],
opts: { afterTool?: boolean; groupTail?: boolean; working?: boolean },
pageVisible: () => boolean,
) {
if (opts.afterTool) return true
if (opts.groupTail === false) return true
if (!pageVisible()) return false
if (opts.working) return false
if (!statuses.length) return false
return !statuses.some((s) => busy(s))
}
function renderable(part: PartType, showReasoningSummaries = true) { 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
@@ -363,7 +349,6 @@ export function AssistantParts(props: {
}) { }) {
const data = useData() const data = useData()
const emptyParts: PartType[] = [] const emptyParts: PartType[] = []
const pageVisible = usePageVisibility()
const groupState = createGroupOpenState() const groupState = createGroupOpenState()
const grouped = createMemo(() => { const grouped = createMemo(() => {
const keys: string[] = [] const keys: string[] = []
@@ -481,24 +466,9 @@ export function AssistantParts(props: {
return COLLAPSIBLE_SPRING return COLLAPSIBLE_SPRING
}) })
const contextOpen = createMemo(() => { const contextOpen = createMemo(() => {
const collapse = (
afterTool?: boolean,
groupTail?: boolean,
group?: { part: ToolPart; message: AssistantMessage }[],
) =>
shouldCollapseGroup(
group?.map((item) => item.part.state.status) ?? [],
{
afterTool,
groupTail,
working: props.working,
},
pageVisible,
)
const value = ctx() const value = ctx()
if (value) return groupState.read(value.groupKey, collapse(value.afterTool, value.tail, value.parts)) if (value) return groupState.read(value.groupKey, true)
const entry = part() return groupState.read(part()?.groupKey, true)
return groupState.read(entry?.groupKey, collapse(entry?.afterTool, entry?.groupTail, entry?.groupParts))
}) })
const visible = createMemo(() => { const visible = createMemo(() => {
if (!context()) return true if (!context()) return true
@@ -544,9 +514,7 @@ export function AssistantParts(props: {
ctxPartsPrev = result ctxPartsPrev = result
return result return result
}) })
const ctxPendingRaw = useContextToolPending(ctxParts, () => !!(props.working && ctx()?.tail)) const ctxPending = useContextToolPending(ctxParts, () => !!(props.working && ctx()?.tail))
const ctxPending = ctxPendingRaw
const ctxHoldOpen = hold(ctxPendingRaw)
const shell = createMemo(() => { const shell = createMemo(() => {
const value = part() const value = part()
if (!value) return if (!value) return
@@ -598,12 +566,20 @@ export function AssistantParts(props: {
onOpenChange={(value: boolean) => groupState.write(entry().groupKey, value)} onOpenChange={(value: boolean) => groupState.write(entry().groupKey, value)}
/> />
</PartGrow> </PartGrow>
<ContextToolExpandedList parts={ctxParts()} expanded={!ctxPending() && contextOpen()} /> <ContextToolExpandedList parts={ctxParts()} expanded={contextOpen() && !ctxPending()} />
<ContextToolRollingResults parts={ctxParts()} pending={ctxHoldOpen()} /> <ContextToolRollingResults parts={ctxParts()} pending={contextOpen() && ctxPending()} />
</> </>
)} )}
</Show> </Show>
<Show when={shell()}>{(value) => <ShellRollingResults part={value()} animate={props.animate} />}</Show> <Show when={shell()}>
{(value) => (
<ShellRollingResults
part={value()}
animate={props.animate}
defaultOpen={props.shellToolDefaultOpen}
/>
)}
</Show>
<Show when={!shell() ? part() : undefined}> <Show when={!shell() ? part() : undefined}>
{(entry) => ( {(entry) => (
<Show when={!entry().context}> <Show when={!entry().context}>
@@ -10,15 +10,7 @@ import { TextShimmer } from "./text-shimmer"
import { Tooltip } from "./tooltip" import { Tooltip } from "./tooltip"
import { GROW_SPRING } from "./motion" import { GROW_SPRING } from "./motion"
import { useSpring } from "./motion-spring" import { useSpring } from "./motion-spring"
import { import { busy, createThrottledValue, updateScrollMask, useCollapsible, useRowWipe, useToolFade } from "./tool-utils"
busy,
createThrottledValue,
hold,
updateScrollMask,
useCollapsible,
useRowWipe,
useToolFade,
} from "./tool-utils"
function ShellRollingSubtitle(props: { text: string; animate?: boolean }) { function ShellRollingSubtitle(props: { text: string; animate?: boolean }) {
let ref: HTMLSpanElement | undefined let ref: HTMLSpanElement | undefined
@@ -176,24 +168,17 @@ function ShellExpanded(props: { cmd: string; out: string; open: boolean }) {
) )
} }
export function ShellRollingResults(props: { part: ToolPart; animate?: boolean }) { export function ShellRollingResults(props: { part: ToolPart; animate?: boolean; defaultOpen?: boolean }) {
const i18n = useI18n() const i18n = useI18n()
const reduce = useReducedMotion() const reduce = useReducedMotion()
const wiped = new Set<string>() const wiped = new Set<string>()
const [mounted, setMounted] = createSignal(false) const [mounted, setMounted] = createSignal(false)
const [userToggled, setUserToggled] = createSignal(false) const [open, setOpen] = createSignal(props.defaultOpen ?? true)
const [userOpen, setUserOpen] = createSignal(false)
onMount(() => setMounted(true)) onMount(() => setMounted(true))
const state = createMemo(() => props.part.state as Record<string, any>) const state = createMemo(() => props.part.state as Record<string, any>)
const pending = createMemo(() => busy(props.part.state.status)) const pending = createMemo(() => busy(props.part.state.status))
const autoOpen = hold(pending, 2000) const expanded = createMemo(() => open() && !pending())
const effectiveOpen = createMemo(() => { const previewOpen = createMemo(() => open() && pending())
if (pending()) return true
if (userToggled()) return userOpen()
return autoOpen()
})
const expanded = createMemo(() => !pending() && !autoOpen() && userToggled() && userOpen())
const previewOpen = createMemo(() => effectiveOpen() && !expanded())
const command = createMemo(() => { const command = createMemo(() => {
const value = state().input?.command ?? state().metadata?.command const value = state().input?.command ?? state().metadata?.command
if (typeof value === "string") return value if (typeof value === "string") return value
@@ -217,12 +202,10 @@ export function ShellRollingResults(props: { part: ToolPart; animate?: boolean }
const headerHeight = useSpring(() => (mounted() ? 37 : 0), GROW_SPRING) const headerHeight = useSpring(() => (mounted() ? 37 : 0), GROW_SPRING)
let headerClipRef: HTMLDivElement | undefined let headerClipRef: HTMLDivElement | undefined
const handleHeaderClick = () => { const handleHeaderClick = () => {
if (pending()) return
const el = headerClipRef const el = headerClipRef
const viewport = el?.closest(".scroll-view__viewport") as HTMLElement | null const viewport = el?.closest(".scroll-view__viewport") as HTMLElement | null
const beforeY = el?.getBoundingClientRect().top ?? 0 const beforeY = el?.getBoundingClientRect().top ?? 0
setUserToggled(true) setOpen((prev) => !prev)
setUserOpen((prev) => !prev)
if (viewport && el) { if (viewport && el) {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const afterY = el.getBoundingClientRect().top const afterY = el.getBoundingClientRect().top
@@ -249,7 +232,7 @@ export function ShellRollingResults(props: { part: ToolPart; animate?: boolean }
ref={headerClipRef} ref={headerClipRef}
data-slot="shell-rolling-header-clip" data-slot="shell-rolling-header-clip"
data-scroll-preserve data-scroll-preserve
data-clickable={!pending() ? "true" : "false"} data-clickable="true"
onClick={handleHeaderClick} onClick={handleHeaderClick}
style={{ height: `${skip() ? (mounted() ? 37 : 0) : headerHeight()}px`, overflow: "clip" }} style={{ height: `${skip() ? (mounted() ? 37 : 0) : headerHeight()}px`, overflow: "clip" }}
> >
@@ -258,13 +241,11 @@ export function ShellRollingResults(props: { part: ToolPart; animate?: boolean }
<TextShimmer text={i18n.t("ui.tool.shell")} active={pending()} /> <TextShimmer text={i18n.t("ui.tool.shell")} active={pending()} />
</span> </span>
<Show when={subtitle()}>{(text) => <ShellRollingSubtitle text={text()} animate={props.animate} />}</Show> <Show when={subtitle()}>{(text) => <ShellRollingSubtitle text={text()} animate={props.animate} />}</Show>
<Show when={!pending()}> <span data-slot="shell-rolling-actions">
<span data-slot="shell-rolling-actions"> <span data-slot="shell-rolling-arrow" data-open={open() ? "true" : "false"}>
<span data-slot="shell-rolling-arrow" data-open={effectiveOpen() ? "true" : "false"}> <Icon name="chevron-down" size="small" />
<Icon name="chevron-down" size="small" />
</span>
</span> </span>
</Show> </span>
</div> </div>
</div> </div>
<div <div