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:
@@ -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" }}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user