Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions web/src/components/SessionFiles/DirectoryTree.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useState } from 'react'
import type { ApiClient } from '@/api/client'
import { FileIcon } from '@/components/FileIcon'
import { useSessionDirectory } from '@/hooks/queries/useSessionDirectory'
Expand Down Expand Up @@ -171,13 +171,40 @@ function DirectoryNode(props: {
)
}

const STORAGE_KEY_PREFIX = 'hapi-dir-expanded-'

function readExpanded(sessionId: string): Set<string> {
try {
const raw = sessionStorage.getItem(STORAGE_KEY_PREFIX + sessionId)
if (raw) {
const parsed = JSON.parse(raw)
if (Array.isArray(parsed)) return new Set(parsed as string[])
}
} catch {
// ignore
}
return new Set([''])
}

function writeExpanded(sessionId: string, expanded: Set<string>) {
try {
sessionStorage.setItem(STORAGE_KEY_PREFIX + sessionId, JSON.stringify([...expanded]))
} catch {
// ignore
}
}

export function DirectoryTree(props: {
api: ApiClient | null
sessionId: string
rootLabel: string
onOpenFile: (path: string) => void
}) {
const [expanded, setExpanded] = useState<Set<string>>(() => new Set(['']))
const [expanded, setExpanded] = useState<Set<string>>(() => readExpanded(props.sessionId))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Major] useState(() => readExpanded(props.sessionId)) only reads storage on the first mount. Because the files route is under the parameterized $sessionId route, navigating from one session's Files page to another can reuse this component with a new sessionId; the old expanded set is then written under the new session key by the effect below. Key the tree by sessionId (or otherwise reset state before writing for a new id) so each session hydrates its own persisted state.

Suggested fix:

<DirectoryTree
    key={sessionId}
    api={api}
    sessionId={sessionId}
    rootLabel={rootLabel}
    onOpenFile={(path) => handleOpenFile(path)}
/>


useEffect(() => {
writeExpanded(props.sessionId, expanded)
}, [props.sessionId, expanded])

const handleToggle = useCallback((path: string) => {
setExpanded((prev) => {
Expand Down
28 changes: 26 additions & 2 deletions web/src/routes/sessions/files.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback, useMemo, useState } from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams, useSearch } from '@tanstack/react-router'
import type { FileSearchItem, GitFileStatus } from '@/types/api'
import { FileIcon } from '@/components/FileIcon'
Expand Down Expand Up @@ -236,6 +236,8 @@ function FileListSkeleton(props: { label: string; rows?: number }) {
)
}

const SCROLL_KEY_PREFIX = 'hapi-dir-scroll-'

export default function FilesPage() {
const { api } = useAppContext()
const { t } = useTranslation()
Expand All @@ -246,10 +248,31 @@ export default function FilesPage() {
const search = useSearch({ from: '/sessions/$sessionId/files' })
const { session } = useSession(api, sessionId)
const [searchQuery, setSearchQuery] = useState('')
const scrollRef = useRef<HTMLDivElement>(null)

const initialTab = search.tab === 'directories' ? 'directories' : 'changes'
const [activeTab, setActiveTab] = useState<'changes' | 'directories'>(initialTab)

useEffect(() => {
const el = scrollRef.current
if (!el) return
const key = SCROLL_KEY_PREFIX + sessionId
try {
const saved = sessionStorage.getItem(key)
if (saved !== null) el.scrollTop = Number(saved)
} catch {
// ignore
}
return () => {
try {
sessionStorage.setItem(key, String(el.scrollTop))
} catch {
// ignore
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId])

const {
status: gitStatus,
error: gitError,
Expand Down Expand Up @@ -411,7 +434,7 @@ export default function FilesPage() {
</div>
) : null}

<div className="app-scroll-y flex-1 min-h-0">
<div ref={scrollRef} className="app-scroll-y flex-1 min-h-0">
<div className="mx-auto w-full max-w-content">
{showGitErrorBanner && activeTab === 'changes' ? (
<div className="border-b border-[var(--app-divider)] bg-amber-500/10 px-3 py-2 text-xs text-[var(--app-hint)]">
Expand Down Expand Up @@ -441,6 +464,7 @@ export default function FilesPage() {
)
) : activeTab === 'directories' ? (
<DirectoryTree
key={sessionId}
api={api}
sessionId={sessionId}
rootLabel={rootLabel}
Expand Down
Loading