feat(web): session header files and outline view toggles (#952)

* feat(web): session header files and outline view toggles

Files and outline icons in SessionHeader act as depressed toggles; files
view shares the session header and places refresh beside the search box.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(web): add Playwright handoff script for session view toggles

Supports new-feature-intake visual gate: files toggle pressed + refresh beside search.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(dev): handoff script avoid networkidle on live hub SSE

HAPI keeps connections open on :3006; domcontentloaded is the correct wait.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(web): place filesystem refresh outside search field

Refresh is a sibling of the search pill, not inside it, so the control
is visually and structurally separate from file search.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
HeavyGee
2026-06-19 17:36:36 +08:00
committed by GitHub
co-authored by Cursor
parent 02a0aa6733
commit b1910b6b2e
7 changed files with 210 additions and 62 deletions
+21 -4
View File
@@ -374,6 +374,8 @@ type SessionChatProps = {
// user dismisses or starts editing.
sendError?: ComposerSendError | null
onClearSendError?: () => void
initialOutlineOpen?: boolean
onInitialOutlineConsumed?: () => void
}
/**
@@ -407,7 +409,15 @@ function SessionChatInner(props: SessionChatProps) {
const blocksByIdRef = useRef<Map<string, ChatBlock>>(new Map())
const visibleGroupsRef = useRef<ToolGroupBlock[]>([])
const [forceScrollToken, setForceScrollToken] = useState(0)
const [outlineOpen, setOutlineOpen] = useState(false)
const [outlineOpen, setOutlineOpen] = useState(props.initialOutlineOpen ?? false)
useEffect(() => {
if (!props.initialOutlineOpen) {
return
}
setOutlineOpen(true)
props.onInitialOutlineConsumed?.()
}, [props.initialOutlineOpen, props.onInitialOutlineConsumed])
const [cursorSelectedBase, setCursorSelectedBase] = useState('auto')
const lastSyncedCursorModelRef = useRef<string | null | undefined>(undefined)
const scratchlist = useScratchlist(props.session.id)
@@ -995,13 +1005,18 @@ function SessionChatInner(props: SessionChatProps) {
props.onRefresh()
}, [switchSession, props.onRefresh])
const handleViewFiles = useCallback(() => {
const handleToggleFiles = useCallback(() => {
setOutlineOpen(false)
navigate({
to: '/sessions/$sessionId/files',
params: { sessionId: props.session.id }
})
}, [navigate, props.session.id])
const handleToggleOutline = useCallback(() => {
setOutlineOpen((open) => !open)
}, [])
const handleViewTerminal = useCallback(() => {
navigate({
to: '/sessions/$sessionId/terminal',
@@ -1089,8 +1104,10 @@ function SessionChatInner(props: SessionChatProps) {
<SessionHeader
session={props.session}
onBack={props.onBack}
onViewFiles={props.session.metadata?.path ? handleViewFiles : undefined}
onOpenOutline={() => setOutlineOpen(true)}
onToggleFiles={props.session.metadata?.path ? handleToggleFiles : undefined}
filesActive={false}
onToggleOutline={handleToggleOutline}
outlineActive={outlineOpen}
api={props.api}
onSessionDeleted={props.onBack}
onSessionReopened={(newSessionId) => {