feat: persist git panel open/closed state to localStorage (#1506)

* feat: persist git panel open/closed state to localStorage

Make the agent thread git panel remember whether the user opened or
closed it. The collapsed state now reads from and writes to
localStorage, so it stays open or closed across thread navigation and
page reloads instead of always re-collapsing per thread.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

* refactor: use plain function and named constants for git panel collapse

Drop the unnecessary useCallback around setCollapsed and replace the
"1"/"0" string literals with COLLAPSED_STATE_TRUE / COLLAPSED_STATE_FALSE
constants.

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Caroline di Vittorio 2026-06-11 17:09:28 -07:00 • committed by GitHub
parent d10cc15e82
commit 3724014d80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -72,6 +72,9 @@ function commonDirPrefix(paths: Array<string>): string {
}
const PANEL_STORAGE_WIDTH = "open-swe.gitpanel.width"
const PANEL_STORAGE_COLLAPSED = "open-swe.gitpanel.collapsed"
const COLLAPSED_STATE_TRUE = "1"
const COLLAPSED_STATE_FALSE = "0"
const PANEL_DEFAULT_WIDTH = 420
const PANEL_MIN_WIDTH = 320
const PANEL_MAX_WIDTH = 720
@ -84,6 +87,14 @@ function readStoredPanelWidth(): number {
return Math.min(PANEL_MAX_WIDTH, Math.max(PANEL_MIN_WIDTH, parsed))
}
function readStoredPanelCollapsed(): boolean {
if (typeof window === "undefined") return true
// Default to collapsed until the user opens it once.
return (
window.localStorage.getItem(PANEL_STORAGE_COLLAPSED) !== COLLAPSED_STATE_FALSE
)
}
function PanelResizeHandle({
width,
onResize,
@ -163,10 +174,22 @@ export function treeThemeStyle(): React.CSSProperties {
export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
const [topTab, setTopTab] = useState<"git" | "desktop" | "terminal">("git")
const [tab, setTab] = useState<"diff" | "review" | "commits">("diff")
const [collapsed, setCollapsed] = useState(true)
const [collapsed, setCollapsedState] = useState(() =>
readStoredPanelCollapsed()
)
const [width, setWidthState] = useState(() => readStoredPanelWidth())
const [fullScreen, setFullScreen] = useState(false)
const setCollapsed = (next: boolean) => {
setCollapsedState(next)
if (typeof window !== "undefined") {
window.localStorage.setItem(
PANEL_STORAGE_COLLAPSED,
next ? COLLAPSED_STATE_TRUE : COLLAPSED_STATE_FALSE
)
}
}
const setWidth = useCallback((next: number) => {
const clamped = Math.min(PANEL_MAX_WIDTH, Math.max(PANEL_MIN_WIDTH, next))
setWidthState(clamped)
@ -176,14 +199,13 @@ export function AgentGitPanel({ thread, messages }: AgentGitPanelProps) {
const sectionRefs = useRef<Record<string, HTMLDivElement | null>>({})
const pr = thread.pr
// Always start collapsed; re-collapse when switching threads, and
// uncollapse when a PR lands mid-session.
// The open/closed state is persisted to localStorage, so it carries across
// threads and reloads. Still uncollapse when a PR lands mid-session.
const [prSeen, setPrSeen] = useState<{ threadId: string; hadPr: boolean }>(
() => ({ threadId: thread.id, hadPr: Boolean(pr) })
)
if (prSeen.threadId !== thread.id) {
setPrSeen({ threadId: thread.id, hadPr: Boolean(pr) })
setCollapsed(true)
} else if (pr && !prSeen.hadPr) {
setPrSeen({ threadId: thread.id, hadPr: true })
setCollapsed(false)