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

View File

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

View File

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