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