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
+55 -47
View File
@@ -3,6 +3,8 @@ import { useNavigate, useParams, useSearch } from '@tanstack/react-router'
import type { FileSearchItem, GitFileStatus } from '@/types/api'
import { FileIcon } from '@/components/FileIcon'
import { DirectoryTree } from '@/components/SessionFiles/DirectoryTree'
import { SessionHeader } from '@/components/SessionHeader'
import { LoadingState } from '@/components/LoadingState'
import { useAppContext } from '@/lib/app-context'
import { useAppGoBack } from '@/hooks/useAppGoBack'
import { useGitStatusFiles } from '@/hooks/queries/useGitStatusFiles'
@@ -19,25 +21,6 @@ import { queryKeys } from '@/lib/query-keys'
import { useQueryClient } from '@tanstack/react-query'
import { useTranslation } from '@/lib/use-translation'
function BackIcon(props: { className?: string }) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={props.className}
>
<polyline points="15 18 9 12 15 6" />
</svg>
)
}
function RefreshIcon(props: { className?: string }) {
return (
<svg
@@ -302,7 +285,6 @@ export default function FilesPage() {
}, [activeTab, navigate, sessionId])
const branchLabel = getDetachedBranchLabel(gitStatus?.branch, t)
const subtitle = session?.metadata?.path ?? sessionId
const showGitErrorBanner = Boolean(gitError)
const gitErrorMessage = useMemo(
() => (gitError ? formatGitStatusError(gitError, t) : null),
@@ -346,45 +328,71 @@ export default function FilesPage() {
})
}, [navigate, sessionId])
const handleToggleFiles = useCallback(() => {
navigate({
to: '/sessions/$sessionId',
params: { sessionId },
})
}, [navigate, sessionId])
const handleToggleOutline = useCallback(() => {
navigate({
to: '/sessions/$sessionId',
params: { sessionId },
search: { outline: true },
})
}, [navigate, sessionId])
if (!session) {
return (
<div className="flex flex-1 items-center justify-center p-4">
<LoadingState label={t('loading.files')} className="text-sm" />
</div>
)
}
return (
<div className="flex h-full min-h-0 flex-col">
<div className="bg-[var(--app-bg)] pt-[env(safe-area-inset-top)]">
<div className="mx-auto w-full max-w-content flex items-center gap-2 p-3 border-b border-[var(--app-border)]">
<button
type="button"
onClick={goBack}
className="flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-hint)] transition-colors hover:bg-[var(--app-secondary-bg)] hover:text-[var(--app-fg)]"
>
<BackIcon />
</button>
<div className="min-w-0 flex-1">
<div className="truncate font-semibold">{t('files.page.title')}</div>
<div className="truncate text-xs text-[var(--app-hint)]">{subtitle}</div>
</div>
<button
type="button"
onClick={handleRefresh}
className="flex h-8 w-8 items-center justify-center rounded-full text-[var(--app-hint)] transition-colors hover:bg-[var(--app-secondary-bg)] hover:text-[var(--app-fg)]"
title={t('files.page.refresh')}
>
<RefreshIcon />
</button>
</div>
</div>
<SessionHeader
session={session}
onBack={goBack}
onToggleFiles={session.metadata?.path ? handleToggleFiles : undefined}
filesActive={true}
onToggleOutline={handleToggleOutline}
outlineActive={false}
api={api}
onSessionDeleted={goBack}
onSessionReopened={(newSessionId) => {
navigate({
to: '/sessions/$sessionId/files',
params: { sessionId: newSessionId },
replace: true,
})
}}
/>
<div className="bg-[var(--app-bg)]">
<div className="mx-auto w-full max-w-content p-3 border-b border-[var(--app-border)]">
<div className="flex items-center gap-2 rounded-md bg-[var(--app-subtle-bg)] px-3 py-2">
<SearchIcon className="text-[var(--app-hint)]" />
<div className="mx-auto flex w-full max-w-content items-center gap-2 border-b border-[var(--app-border)] p-3">
<div className="flex min-w-0 flex-1 items-center gap-2 rounded-md bg-[var(--app-subtle-bg)] px-3 py-2">
<SearchIcon className="shrink-0 text-[var(--app-hint)]" />
<input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}
placeholder={t('files.page.searchPlaceholder')}
className="w-full bg-transparent text-sm text-[var(--app-fg)] placeholder:text-[var(--app-hint)] focus:outline-none"
className="min-w-0 flex-1 bg-transparent text-sm text-[var(--app-fg)] placeholder:text-[var(--app-hint)] focus:outline-none"
autoCapitalize="none"
autoCorrect="off"
/>
</div>
<button
type="button"
onClick={handleRefresh}
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-[var(--app-hint)] transition-colors hover:bg-[var(--app-subtle-bg)] hover:text-[var(--app-fg)]"
title={t('files.page.refreshFilesystem')}
aria-label={t('files.page.refreshFilesystem')}
>
<RefreshIcon />
</button>
</div>
</div>