fix(ui): keep right panel layout in empty states

Render SplitFilePanel consistently and move empty/loading messages into the viewer area so the right drawer keeps its standard layout even when there are no session diffs, no git changes, or files are still loading.
This commit is contained in:
Shantur Rathore
2026-02-11 10:51:27 +00:00
parent a93252621a
commit d34e0163e3
3 changed files with 160 additions and 164 deletions

View File

@@ -32,32 +32,11 @@ interface ChangesTabProps {
const ChangesTab: Component<ChangesTabProps> = (props) => { const ChangesTab: Component<ChangesTabProps> = (props) => {
const renderContent = (): JSX.Element => { const renderContent = (): JSX.Element => {
const sessionId = props.activeSessionId() const sessionId = props.activeSessionId()
if (!sessionId || sessionId === "info") {
return (
<div class="right-panel-empty">
<span class="text-xs">{props.t("instanceShell.sessionChanges.noSessionSelected")}</span>
</div>
)
}
const diffs = props.activeSessionDiffs() const hasSession = Boolean(sessionId && sessionId !== "info")
if (diffs === undefined) { const diffs = hasSession ? props.activeSessionDiffs() : null
return (
<div class="right-panel-empty">
<span class="text-xs">{props.t("instanceShell.sessionChanges.loading")}</span>
</div>
)
}
if (!Array.isArray(diffs) || diffs.length === 0) { const sorted = Array.isArray(diffs) ? [...diffs].sort((a, b) => String(a.file || "").localeCompare(String(b.file || ""))) : []
return (
<div class="right-panel-empty">
<span class="text-xs">{props.t("instanceShell.sessionChanges.empty")}</span>
</div>
)
}
const sorted = [...diffs].sort((a, b) => String(a.file || "").localeCompare(String(b.file || "")))
const totals = sorted.reduce( const totals = sorted.reduce(
(acc, item) => { (acc, item) => {
acc.additions += typeof item.additions === "number" ? item.additions : 0 acc.additions += typeof item.additions === "number" ? item.additions : 0
@@ -67,25 +46,27 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
{ additions: 0, deletions: 0 }, { additions: 0, deletions: 0 },
) )
const mostChanged = sorted.reduce((best, item) => { const mostChanged = sorted.length
const bestAdd = typeof (best as any)?.additions === "number" ? (best as any).additions : 0 ? sorted.reduce((best, item) => {
const bestDel = typeof (best as any)?.deletions === "number" ? (best as any).deletions : 0 const bestAdd = typeof (best as any)?.additions === "number" ? (best as any).additions : 0
const bestScore = bestAdd + bestDel const bestDel = typeof (best as any)?.deletions === "number" ? (best as any).deletions : 0
const bestScore = bestAdd + bestDel
const add = typeof (item as any)?.additions === "number" ? (item as any).additions : 0 const add = typeof (item as any)?.additions === "number" ? (item as any).additions : 0
const del = typeof (item as any)?.deletions === "number" ? (item as any).deletions : 0 const del = typeof (item as any)?.deletions === "number" ? (item as any).deletions : 0
const score = add + del const score = add + del
if (score > bestScore) return item if (score > bestScore) return item
if (score < bestScore) return best if (score < bestScore) return best
return String(item.file || "").localeCompare(String((best as any)?.file || "")) < 0 ? item : best return String(item.file || "").localeCompare(String((best as any)?.file || "")) < 0 ? item : best
}, sorted[0]) }, sorted[0])
: null
// Auto-select the most-changed file if none selected. // Auto-select the most-changed file if none selected.
const currentSelected = props.selectedFile() const currentSelected = props.selectedFile()
const selectedFileData = sorted.find((f) => f.file === currentSelected) || mostChanged const selectedFileData = sorted.find((f) => f.file === currentSelected) || mostChanged
const scopeKey = `${props.instanceId}:${sessionId}` const scopeKey = `${props.instanceId}:${hasSession ? sessionId : "no-session"}`
const isBinaryDiff = (item: any) => { const isBinaryDiff = (item: any) => {
const before = typeof item?.before === "string" ? item.before : "" const before = typeof item?.before === "string" ? item.before : ""
@@ -97,6 +78,13 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
return false return false
} }
const emptyViewerMessage = () => {
if (!hasSession) return props.t("instanceShell.sessionChanges.noSessionSelected")
if (diffs === undefined) return props.t("instanceShell.sessionChanges.loading")
if (!Array.isArray(diffs) || diffs.length === 0) return props.t("instanceShell.sessionChanges.empty")
return props.t("instanceShell.filesShell.viewerEmpty")
}
const renderViewer = () => ( const renderViewer = () => (
<div class="file-viewer-panel flex-1"> <div class="file-viewer-panel flex-1">
<div class="file-viewer-header"> <div class="file-viewer-header">
@@ -109,10 +97,10 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
</div> </div>
<div class="file-viewer-content file-viewer-content--monaco"> <div class="file-viewer-content file-viewer-content--monaco">
<Show <Show
when={selectedFileData} when={selectedFileData && hasSession && Array.isArray(diffs) && diffs.length > 0}
fallback={ fallback={
<div class="file-viewer-empty"> <div class="file-viewer-empty">
<span class="file-viewer-empty-text">{props.t("instanceShell.filesShell.viewerEmpty")}</span> <span class="file-viewer-empty-text">{emptyViewerMessage()}</span>
</div> </div>
} }
> >
@@ -140,59 +128,69 @@ const ChangesTab: Component<ChangesTabProps> = (props) => {
</div> </div>
) )
const renderEmptyList = () => (
<div class="p-3 text-xs text-secondary">{emptyViewerMessage()}</div>
)
const renderListPanel = () => ( const renderListPanel = () => (
<For each={sorted}> <Show when={sorted.length > 0} fallback={renderEmptyList()}>
{(item) => ( <For each={sorted}>
<div {(item) => (
class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`} <div
onClick={() => { class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`}
props.onSelectFile(item.file, props.isPhoneLayout()) onClick={() => {
}} props.onSelectFile(item.file, props.isPhoneLayout())
> }}
<div class="file-list-item-content"> >
<div class="file-list-item-path" title={item.file}> <div class="file-list-item-content">
{item.file} <div class="file-list-item-path" title={item.file}>
</div> {item.file}
<div class="file-list-item-stats"> </div>
<span class="file-list-item-additions">+{item.additions}</span> <div class="file-list-item-stats">
<span class="file-list-item-deletions">-{item.deletions}</span> <span class="file-list-item-additions">+{item.additions}</span>
<span class="file-list-item-deletions">-{item.deletions}</span>
</div>
</div> </div>
</div> </div>
</div> )}
)} </For>
</For> </Show>
) )
const renderListOverlay = () => ( const renderListOverlay = () => (
<For each={sorted}> <Show when={sorted.length > 0} fallback={renderEmptyList()}>
{(item) => ( <For each={sorted}>
<div {(item) => (
class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`} <div
onClick={() => { class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`}
props.onSelectFile(item.file, true) onClick={() => {
}} props.onSelectFile(item.file, true)
title={item.file} }}
> title={item.file}
<div class="file-list-item-content"> >
<div class="file-list-item-path" title={item.file}> <div class="file-list-item-content">
{item.file} <div class="file-list-item-path" title={item.file}>
</div> {item.file}
<div class="file-list-item-stats"> </div>
<span class="file-list-item-additions">+{item.additions}</span> <div class="file-list-item-stats">
<span class="file-list-item-deletions">-{item.deletions}</span> <span class="file-list-item-additions">+{item.additions}</span>
<span class="file-list-item-deletions">-{item.deletions}</span>
</div>
</div> </div>
</div> </div>
</div> )}
)} </For>
</For> </Show>
) )
const headerPath = () => (selectedFileData?.file ? selectedFileData.file : props.t("instanceShell.rightPanel.tabs.changes"))
return ( return (
<SplitFilePanel <SplitFilePanel
header={ header={
<> <>
<span class="files-tab-selected-path" title={selectedFileData?.file || ""}> <span class="files-tab-selected-path" title={headerPath()}>
{selectedFileData?.file || ""} {headerPath()}
</span> </span>
<div class="files-tab-stats" style={{ flex: "0 0 auto" }}> <div class="files-tab-stats" style={{ flex: "0 0 auto" }}>

View File

@@ -37,15 +37,8 @@ interface FilesTabProps {
const FilesTab: Component<FilesTabProps> = (props) => { const FilesTab: Component<FilesTabProps> = (props) => {
const renderContent = (): JSX.Element => { const renderContent = (): JSX.Element => {
if (props.browserLoading() && props.browserEntries() === null) { const entriesValue = props.browserEntries()
return ( const entries = entriesValue || []
<div class="right-panel-empty">
<span class="text-xs">Loading files...</span>
</div>
)
}
const entries = props.browserEntries() || []
const sorted = [...entries].sort((a, b) => { const sorted = [...entries].sort((a, b) => {
const aDir = a.type === "directory" ? 0 : 1 const aDir = a.type === "directory" ? 0 : 1
const bDir = b.type === "directory" ? 0 : 1 const bDir = b.type === "directory" ? 0 : 1
@@ -57,6 +50,11 @@ const FilesTab: Component<FilesTabProps> = (props) => {
const headerDisplayedPath = () => props.browserSelectedPath() || props.browserPath() const headerDisplayedPath = () => props.browserSelectedPath() || props.browserPath()
const emptyViewerMessage = () => {
if (props.browserLoading() && entriesValue === null) return "Loading files..."
return "Select a file to preview"
}
const renderViewer = () => ( const renderViewer = () => (
<div class="file-viewer-panel flex-1"> <div class="file-viewer-panel flex-1">
<div class="file-viewer-content file-viewer-content--monaco"> <div class="file-viewer-content file-viewer-content--monaco">
@@ -74,7 +72,7 @@ const FilesTab: Component<FilesTabProps> = (props) => {
} }
fallback={ fallback={
<div class="file-viewer-empty"> <div class="file-viewer-empty">
<span class="file-viewer-empty-text">Select a file to preview</span> <span class="file-viewer-empty-text">{emptyViewerMessage()}</span>
</div> </div>
} }
> >
@@ -114,6 +112,10 @@ const FilesTab: Component<FilesTabProps> = (props) => {
)} )}
</Show> </Show>
<Show when={props.browserLoading() && entriesValue === null}>
<div class="p-3 text-xs text-secondary">Loading files...</div>
</Show>
<For each={sorted}> <For each={sorted}>
{(item) => ( {(item) => (
<div <div

View File

@@ -46,33 +46,14 @@ interface GitChangesTabProps {
const GitChangesTab: Component<GitChangesTabProps> = (props) => { const GitChangesTab: Component<GitChangesTabProps> = (props) => {
const renderContent = (): JSX.Element => { const renderContent = (): JSX.Element => {
const sessionId = props.activeSessionId() const sessionId = props.activeSessionId()
if (!sessionId || sessionId === "info") {
return (
<div class="right-panel-empty">
<span class="text-xs">Select a session to view changes.</span>
</div>
)
}
const entries = props.entries() const hasSession = Boolean(sessionId && sessionId !== "info")
if (entries === null) { const entries = hasSession ? props.entries() : null
return (
<div class="right-panel-empty">
<span class="text-xs">Loading git changes</span>
</div>
)
}
const nonDeleted = entries.filter((item) => item && item.status !== "deleted") const sorted = Array.isArray(entries)
if (nonDeleted.length === 0) { ? [...entries].sort((a, b) => String(a.path || "").localeCompare(String(b.path || "")))
return ( : []
<div class="right-panel-empty">
<span class="text-xs">No git changes yet.</span>
</div>
)
}
const sorted = [...entries].sort((a, b) => String(a.path || "").localeCompare(String(b.path || "")))
const totals = sorted.reduce( const totals = sorted.reduce(
(acc, item) => { (acc, item) => {
acc.additions += typeof item.added === "number" ? item.added : 0 acc.additions += typeof item.added === "number" ? item.added : 0
@@ -82,6 +63,15 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
{ additions: 0, deletions: 0 }, { additions: 0, deletions: 0 },
) )
const nonDeleted = sorted.filter((item) => item && item.status !== "deleted")
const emptyViewerMessage = () => {
if (!hasSession) return "Select a session to view changes."
if (entries === null) return "Loading git changes…"
if (nonDeleted.length === 0) return "No git changes yet."
return "No file selected."
}
const selectedPath = props.selectedPath() const selectedPath = props.selectedPath()
const fallbackPath = props.mostChangedPath() const fallbackPath = props.mostChangedPath()
const selectedEntry = const selectedEntry =
@@ -120,7 +110,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
} }
fallback={ fallback={
<div class="file-viewer-empty"> <div class="file-viewer-empty">
<span class="file-viewer-empty-text">No file selected.</span> <span class="file-viewer-empty-text">{emptyViewerMessage()}</span>
</div> </div>
} }
> >
@@ -153,71 +143,77 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
</div> </div>
) )
const renderEmptyList = () => <div class="p-3 text-xs text-secondary">{emptyViewerMessage()}</div>
const renderListPanel = () => ( const renderListPanel = () => (
<For each={sorted}> <Show when={nonDeleted.length > 0} fallback={renderEmptyList()}>
{(item) => ( <For each={sorted}>
<div {(item) => (
class={`file-list-item ${props.selectedPath() === item.path ? "file-list-item-active" : ""}`} <div
onClick={() => { class={`file-list-item ${props.selectedPath() === item.path ? "file-list-item-active" : ""}`}
props.onOpenFile(item.path) onClick={() => {
}} props.onOpenFile(item.path)
> }}
<div class="file-list-item-content"> >
<div class="file-list-item-path" title={item.path}> <div class="file-list-item-content">
{item.path} <div class="file-list-item-path" title={item.path}>
</div> {item.path}
<div class="file-list-item-stats"> </div>
<Show when={item.status === "deleted"}> <div class="file-list-item-stats">
<span class="text-[10px] text-secondary">deleted</span> <Show when={item.status === "deleted"}>
</Show> <span class="text-[10px] text-secondary">deleted</span>
<Show when={item.status !== "deleted"}> </Show>
<> <Show when={item.status !== "deleted"}>
<span class="file-list-item-additions">+{item.added}</span> <>
<span class="file-list-item-deletions">-{item.removed}</span> <span class="file-list-item-additions">+{item.added}</span>
</> <span class="file-list-item-deletions">-{item.removed}</span>
</Show> </>
</Show>
</div>
</div> </div>
</div> </div>
</div> )}
)} </For>
</For> </Show>
) )
const renderListOverlay = () => ( const renderListOverlay = () => (
<For each={sorted}> <Show when={nonDeleted.length > 0} fallback={renderEmptyList()}>
{(item) => ( <For each={sorted}>
<div {(item) => (
class={`file-list-item ${props.selectedPath() === item.path ? "file-list-item-active" : ""}`} <div
onClick={() => props.onOpenFile(item.path)} class={`file-list-item ${props.selectedPath() === item.path ? "file-list-item-active" : ""}`}
title={item.path} onClick={() => props.onOpenFile(item.path)}
> title={item.path}
<div class="file-list-item-content"> >
<div class="file-list-item-path" title={item.path}> <div class="file-list-item-content">
{item.path} <div class="file-list-item-path" title={item.path}>
</div> {item.path}
<div class="file-list-item-stats"> </div>
<Show when={item.status === "deleted"}> <div class="file-list-item-stats">
<span class="text-[10px] text-secondary">deleted</span> <Show when={item.status === "deleted"}>
</Show> <span class="text-[10px] text-secondary">deleted</span>
<Show when={item.status !== "deleted"}> </Show>
<> <Show when={item.status !== "deleted"}>
<span class="file-list-item-additions">+{item.added}</span> <>
<span class="file-list-item-deletions">-{item.removed}</span> <span class="file-list-item-additions">+{item.added}</span>
</> <span class="file-list-item-deletions">-{item.removed}</span>
</Show> </>
</Show>
</div>
</div> </div>
</div> </div>
</div> )}
)} </For>
</For> </Show>
) )
return ( return (
<SplitFilePanel <SplitFilePanel
header={ header={
<> <>
<span class="files-tab-selected-path" title={selectedEntry?.path || ""}> <span class="files-tab-selected-path" title={selectedEntry?.path || "Git Changes"}>
{selectedEntry?.path || ""} {selectedEntry?.path || "Git Changes"}
</span> </span>
<div class="files-tab-stats" style={{ flex: "0 0 auto" }}> <div class="files-tab-stats" style={{ flex: "0 0 auto" }}>
@@ -235,7 +231,7 @@ const GitChangesTab: Component<GitChangesTabProps> = (props) => {
class="files-header-icon-button" class="files-header-icon-button"
title={props.t("instanceShell.rightPanel.actions.refresh")} title={props.t("instanceShell.rightPanel.actions.refresh")}
aria-label={props.t("instanceShell.rightPanel.actions.refresh")} aria-label={props.t("instanceShell.rightPanel.actions.refresh")}
disabled={props.statusLoading()} disabled={!hasSession || props.statusLoading() || entries === null}
style={{ "margin-left": "auto" }} style={{ "margin-left": "auto" }}
onClick={() => props.onRefresh()} onClick={() => props.onRefresh()}
> >