mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-07 06:52:28 +00:00
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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user