feat(ui): improve right panel changes/files layout
This commit is contained in:
@@ -104,13 +104,22 @@ const LEFT_DRAWER_STORAGE_KEY = "opencode-session-sidebar-width-v8"
|
|||||||
const RIGHT_DRAWER_STORAGE_KEY = "opencode-session-right-drawer-width-v1"
|
const RIGHT_DRAWER_STORAGE_KEY = "opencode-session-right-drawer-width-v1"
|
||||||
const LEFT_PIN_STORAGE_KEY = "opencode-session-left-drawer-pinned-v1"
|
const LEFT_PIN_STORAGE_KEY = "opencode-session-left-drawer-pinned-v1"
|
||||||
const RIGHT_PIN_STORAGE_KEY = "opencode-session-right-drawer-pinned-v1"
|
const RIGHT_PIN_STORAGE_KEY = "opencode-session-right-drawer-pinned-v1"
|
||||||
const RIGHT_PANEL_TAB_STORAGE_KEY = "opencode-session-right-panel-tab-v1"
|
const RIGHT_PANEL_TAB_STORAGE_KEY = "opencode-session-right-panel-tab-v2"
|
||||||
|
const LEGACY_RIGHT_PANEL_TAB_STORAGE_KEY = "opencode-session-right-panel-tab-v1"
|
||||||
|
const RIGHT_PANEL_CHANGES_SPLIT_WIDTH_KEY = "opencode-session-right-panel-changes-split-width-v1"
|
||||||
|
const RIGHT_PANEL_FILES_SPLIT_WIDTH_KEY = "opencode-session-right-panel-files-split-width-v1"
|
||||||
|
const RIGHT_PANEL_CHANGES_LIST_OPEN_NONPHONE_KEY = "opencode-session-right-panel-changes-list-open-nonphone-v1"
|
||||||
|
const RIGHT_PANEL_CHANGES_LIST_OPEN_PHONE_KEY = "opencode-session-right-panel-changes-list-open-phone-v1"
|
||||||
|
const RIGHT_PANEL_FILES_LIST_OPEN_NONPHONE_KEY = "opencode-session-right-panel-files-list-open-nonphone-v1"
|
||||||
|
const RIGHT_PANEL_FILES_LIST_OPEN_PHONE_KEY = "opencode-session-right-panel-files-list-open-phone-v1"
|
||||||
|
const RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY = "opencode-session-right-panel-changes-diff-view-mode-v1"
|
||||||
|
const RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY = "opencode-session-right-panel-changes-diff-context-mode-v1"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
type LayoutMode = "desktop" | "tablet" | "phone"
|
type LayoutMode = "desktop" | "tablet" | "phone"
|
||||||
type RightPanelTab = "files" | "browser" | "status"
|
type RightPanelTab = "changes" | "files" | "status"
|
||||||
|
|
||||||
const clampWidth = (value: number) => Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
|
const clampWidth = (value: number) => Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
|
||||||
const clampRightWidth = (value: number) => {
|
const clampRightWidth = (value: number) => {
|
||||||
@@ -133,12 +142,44 @@ function persistPinState(side: "left" | "right", value: boolean) {
|
|||||||
|
|
||||||
function readStoredRightPanelTab(defaultValue: RightPanelTab): RightPanelTab {
|
function readStoredRightPanelTab(defaultValue: RightPanelTab): RightPanelTab {
|
||||||
if (typeof window === "undefined") return defaultValue
|
if (typeof window === "undefined") return defaultValue
|
||||||
|
|
||||||
const stored = window.localStorage.getItem(RIGHT_PANEL_TAB_STORAGE_KEY)
|
const stored = window.localStorage.getItem(RIGHT_PANEL_TAB_STORAGE_KEY)
|
||||||
if (stored === "status") return "status"
|
if (stored === "status") return "status"
|
||||||
if (stored === "browser") return "browser"
|
if (stored === "changes") return "changes"
|
||||||
|
if (stored === "files") return "files"
|
||||||
|
|
||||||
|
// Migrate from v1 (where the stored values were the internal tab ids).
|
||||||
|
const legacy = window.localStorage.getItem(LEGACY_RIGHT_PANEL_TAB_STORAGE_KEY)
|
||||||
|
if (legacy === "status") return "status"
|
||||||
|
if (legacy === "browser") return "files"
|
||||||
|
if (legacy === "files") return "changes"
|
||||||
|
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readStoredPanelWidth(key: string, fallback: number) {
|
||||||
|
if (typeof window === "undefined") return fallback
|
||||||
|
const stored = window.localStorage.getItem(key)
|
||||||
|
if (!stored) return fallback
|
||||||
|
const parsed = Number.parseInt(stored, 10)
|
||||||
|
return Number.isFinite(parsed) ? parsed : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStoredBool(key: string): boolean | null {
|
||||||
|
if (typeof window === "undefined") return null
|
||||||
|
const stored = window.localStorage.getItem(key)
|
||||||
|
if (stored === "true") return true
|
||||||
|
if (stored === "false") return false
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function readStoredEnum<T extends string>(key: string, allowed: readonly T[]): T | null {
|
||||||
|
if (typeof window === "undefined") return null
|
||||||
|
const stored = window.localStorage.getItem(key)
|
||||||
|
if (!stored) return null
|
||||||
|
return (allowed as readonly string[]).includes(stored) ? (stored as T) : null
|
||||||
|
}
|
||||||
|
|
||||||
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -162,7 +203,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | null>(null)
|
const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | null>(null)
|
||||||
const [resizeStartX, setResizeStartX] = createSignal(0)
|
const [resizeStartX, setResizeStartX] = createSignal(0)
|
||||||
const [resizeStartWidth, setResizeStartWidth] = createSignal(0)
|
const [resizeStartWidth, setResizeStartWidth] = createSignal(0)
|
||||||
const [rightPanelTab, setRightPanelTab] = createSignal<RightPanelTab>(readStoredRightPanelTab("files"))
|
const [rightPanelTab, setRightPanelTab] = createSignal<RightPanelTab>(readStoredRightPanelTab("changes"))
|
||||||
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>([
|
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>([
|
||||||
"plan",
|
"plan",
|
||||||
"background-processes",
|
"background-processes",
|
||||||
@@ -181,8 +222,33 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const [browserSelectedLoading, setBrowserSelectedLoading] = createSignal(false)
|
const [browserSelectedLoading, setBrowserSelectedLoading] = createSignal(false)
|
||||||
const [browserSelectedError, setBrowserSelectedError] = createSignal<string | null>(null)
|
const [browserSelectedError, setBrowserSelectedError] = createSignal<string | null>(null)
|
||||||
|
|
||||||
const [diffViewMode, setDiffViewMode] = createSignal<"split" | "unified">("split")
|
const [diffViewMode, setDiffViewMode] = createSignal<"split" | "unified">(
|
||||||
const [diffContextMode, setDiffContextMode] = createSignal<"expanded" | "collapsed">("collapsed")
|
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY, ["split", "unified"] as const) ?? "unified",
|
||||||
|
)
|
||||||
|
const [diffContextMode, setDiffContextMode] = createSignal<"expanded" | "collapsed">(
|
||||||
|
readStoredEnum(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, ["expanded", "collapsed"] as const) ?? "collapsed",
|
||||||
|
)
|
||||||
|
|
||||||
|
const [changesSplitWidth, setChangesSplitWidth] = createSignal(320)
|
||||||
|
const [filesSplitWidth, setFilesSplitWidth] = createSignal(320)
|
||||||
|
const [activeSplitResize, setActiveSplitResize] = createSignal<"changes" | "files" | null>(null)
|
||||||
|
const [splitResizeStartX, setSplitResizeStartX] = createSignal(0)
|
||||||
|
const [splitResizeStartWidth, setSplitResizeStartWidth] = createSignal(0)
|
||||||
|
|
||||||
|
const [filesListOpen, setFilesListOpen] = createSignal(true)
|
||||||
|
const [filesListTouched, setFilesListTouched] = createSignal(false)
|
||||||
|
const [changesListOpen, setChangesListOpen] = createSignal(true)
|
||||||
|
const [changesListTouched, setChangesListTouched] = createSignal(false)
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
// Default behavior: when nothing is selected, keep the file list open.
|
||||||
|
// Once the user explicitly toggles it, we stop auto-opening.
|
||||||
|
if (rightPanelTab() !== "files") return
|
||||||
|
if (filesListTouched()) return
|
||||||
|
if (!browserSelectedPath()) {
|
||||||
|
setFilesListOpen(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
|
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
|
||||||
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
|
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
|
||||||
@@ -207,6 +273,45 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||||
const rightPinningSupported = createMemo(() => layoutMode() !== "phone")
|
const rightPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||||
|
|
||||||
|
const listLayoutKey = createMemo(() => (isPhoneLayout() ? "phone" : "nonphone"))
|
||||||
|
|
||||||
|
const listOpenStorageKey = (tab: "changes" | "files") => {
|
||||||
|
const layout = listLayoutKey()
|
||||||
|
if (tab === "changes") {
|
||||||
|
return layout === "phone" ? RIGHT_PANEL_CHANGES_LIST_OPEN_PHONE_KEY : RIGHT_PANEL_CHANGES_LIST_OPEN_NONPHONE_KEY
|
||||||
|
}
|
||||||
|
return layout === "phone" ? RIGHT_PANEL_FILES_LIST_OPEN_PHONE_KEY : RIGHT_PANEL_FILES_LIST_OPEN_NONPHONE_KEY
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistListOpen = (tab: "changes" | "files", value: boolean) => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
window.localStorage.setItem(listOpenStorageKey(tab), value ? "true" : "false")
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
// Refresh persisted visibility when layout changes (phone vs non-phone).
|
||||||
|
const layout = listLayoutKey()
|
||||||
|
layout
|
||||||
|
|
||||||
|
const filesPersisted = readStoredBool(listOpenStorageKey("files"))
|
||||||
|
if (filesPersisted !== null) {
|
||||||
|
setFilesListOpen(filesPersisted)
|
||||||
|
setFilesListTouched(true)
|
||||||
|
} else {
|
||||||
|
setFilesListOpen(true)
|
||||||
|
setFilesListTouched(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changesPersisted = readStoredBool(listOpenStorageKey("changes"))
|
||||||
|
if (changesPersisted !== null) {
|
||||||
|
setChangesListOpen(changesPersisted)
|
||||||
|
setChangesListTouched(true)
|
||||||
|
} else {
|
||||||
|
setChangesListOpen(true)
|
||||||
|
setChangesListTouched(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const persistPinIfSupported = (side: "left" | "right", value: boolean) => {
|
const persistPinIfSupported = (side: "left" | "right", value: boolean) => {
|
||||||
if (side === "left" && !leftPinningSupported()) return
|
if (side === "left" && !leftPinningSupported()) return
|
||||||
if (side === "right" && !rightPinningSupported()) return
|
if (side === "right" && !rightPinningSupported()) return
|
||||||
@@ -281,6 +386,9 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
setRightDrawerWidth(clampRightWidth(window.innerWidth * 0.35))
|
setRightDrawerWidth(clampRightWidth(window.innerWidth * 0.35))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setChangesSplitWidth(clampSplitWidth(readStoredPanelWidth(RIGHT_PANEL_CHANGES_SPLIT_WIDTH_KEY, 320)))
|
||||||
|
setFilesSplitWidth(clampSplitWidth(readStoredPanelWidth(RIGHT_PANEL_FILES_SPLIT_WIDTH_KEY, 320)))
|
||||||
|
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
const width = clampWidth(window.innerWidth * 0.3)
|
const width = clampWidth(window.innerWidth * 0.3)
|
||||||
setSessionSidebarWidth((current) => clampWidth(current || width))
|
setSessionSidebarWidth((current) => clampWidth(current || width))
|
||||||
@@ -320,6 +428,16 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
window.localStorage.setItem(RIGHT_PANEL_TAB_STORAGE_KEY, rightPanelTab())
|
window.localStorage.setItem(RIGHT_PANEL_TAB_STORAGE_KEY, rightPanelTab())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_VIEW_MODE_KEY, diffViewMode())
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
window.localStorage.setItem(RIGHT_PANEL_CHANGES_DIFF_CONTEXT_MODE_KEY, diffContextMode())
|
||||||
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
props.tabBarOffset
|
props.tabBarOffset
|
||||||
requestAnimationFrame(() => measureDrawerHost())
|
requestAnimationFrame(() => measureDrawerHost())
|
||||||
@@ -765,6 +883,95 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
stopDrawerResize()
|
stopDrawerResize()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const clampSplitWidth = (value: number) => {
|
||||||
|
const min = 200
|
||||||
|
const maxByDrawer = Math.max(min, Math.floor(rightDrawerWidth() * 0.65))
|
||||||
|
const max = Math.min(560, maxByDrawer)
|
||||||
|
return Math.min(max, Math.max(min, Math.floor(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
const persistSplitWidth = (mode: "changes" | "files", width: number) => {
|
||||||
|
if (typeof window === "undefined") return
|
||||||
|
const key = mode === "changes" ? RIGHT_PANEL_CHANGES_SPLIT_WIDTH_KEY : RIGHT_PANEL_FILES_SPLIT_WIDTH_KEY
|
||||||
|
window.localStorage.setItem(key, String(width))
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopSplitResize() {
|
||||||
|
setActiveSplitResize(null)
|
||||||
|
if (typeof document === "undefined") return
|
||||||
|
document.removeEventListener("mousemove", splitMouseMove)
|
||||||
|
document.removeEventListener("mouseup", splitMouseUp)
|
||||||
|
document.removeEventListener("touchmove", splitTouchMove)
|
||||||
|
document.removeEventListener("touchend", splitTouchEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMouseMove(event: MouseEvent) {
|
||||||
|
const mode = activeSplitResize()
|
||||||
|
if (!mode) return
|
||||||
|
event.preventDefault()
|
||||||
|
const delta = event.clientX - splitResizeStartX()
|
||||||
|
const next = clampSplitWidth(splitResizeStartWidth() + delta)
|
||||||
|
if (mode === "changes") setChangesSplitWidth(next)
|
||||||
|
else setFilesSplitWidth(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitMouseUp() {
|
||||||
|
const mode = activeSplitResize()
|
||||||
|
if (mode) {
|
||||||
|
const width = mode === "changes" ? changesSplitWidth() : filesSplitWidth()
|
||||||
|
persistSplitWidth(mode, width)
|
||||||
|
}
|
||||||
|
stopSplitResize()
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitTouchMove(event: TouchEvent) {
|
||||||
|
const mode = activeSplitResize()
|
||||||
|
if (!mode) return
|
||||||
|
const touch = event.touches[0]
|
||||||
|
if (!touch) return
|
||||||
|
event.preventDefault()
|
||||||
|
const delta = touch.clientX - splitResizeStartX()
|
||||||
|
const next = clampSplitWidth(splitResizeStartWidth() + delta)
|
||||||
|
if (mode === "changes") setChangesSplitWidth(next)
|
||||||
|
else setFilesSplitWidth(next)
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitTouchEnd() {
|
||||||
|
const mode = activeSplitResize()
|
||||||
|
if (mode) {
|
||||||
|
const width = mode === "changes" ? changesSplitWidth() : filesSplitWidth()
|
||||||
|
persistSplitWidth(mode, width)
|
||||||
|
}
|
||||||
|
stopSplitResize()
|
||||||
|
}
|
||||||
|
|
||||||
|
const startSplitResize = (mode: "changes" | "files", clientX: number) => {
|
||||||
|
if (typeof document === "undefined") return
|
||||||
|
setActiveSplitResize(mode)
|
||||||
|
setSplitResizeStartX(clientX)
|
||||||
|
setSplitResizeStartWidth(mode === "changes" ? changesSplitWidth() : filesSplitWidth())
|
||||||
|
document.addEventListener("mousemove", splitMouseMove)
|
||||||
|
document.addEventListener("mouseup", splitMouseUp)
|
||||||
|
document.addEventListener("touchmove", splitTouchMove, { passive: false })
|
||||||
|
document.addEventListener("touchend", splitTouchEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSplitResizeMouseDown = (mode: "changes" | "files") => (event: MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
startSplitResize(mode, event.clientX)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSplitResizeTouchStart = (mode: "changes" | "files") => (event: TouchEvent) => {
|
||||||
|
const touch = event.touches[0]
|
||||||
|
if (!touch) return
|
||||||
|
event.preventDefault()
|
||||||
|
startSplitResize(mode, touch.clientX)
|
||||||
|
}
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
stopSplitResize()
|
||||||
|
})
|
||||||
|
|
||||||
type DrawerViewState = "pinned" | "floating-open" | "floating-closed"
|
type DrawerViewState = "pinned" | "floating-open" | "floating-closed"
|
||||||
|
|
||||||
|
|
||||||
@@ -1035,6 +1242,36 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
|
|
||||||
const browserClient = createMemo(() => getOrCreateWorktreeClient(props.instance.id, worktreeSlugForViewer()))
|
const browserClient = createMemo(() => getOrCreateWorktreeClient(props.instance.id, worktreeSlugForViewer()))
|
||||||
|
|
||||||
|
const bestDiffFile = createMemo<string | null>(() => {
|
||||||
|
const diffs = activeSessionDiffs()
|
||||||
|
if (!Array.isArray(diffs) || diffs.length === 0) return null
|
||||||
|
const best = diffs.reduce((currentBest, item) => {
|
||||||
|
const bestAdd = typeof (currentBest as any)?.additions === "number" ? (currentBest as any).additions : 0
|
||||||
|
const bestDel = typeof (currentBest as any)?.deletions === "number" ? (currentBest 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
|
||||||
|
|
||||||
|
if (score > bestScore) return item
|
||||||
|
if (score < bestScore) return currentBest
|
||||||
|
return String(item.file || "").localeCompare(String((currentBest as any)?.file || "")) < 0 ? item : currentBest
|
||||||
|
}, diffs[0])
|
||||||
|
return typeof (best as any)?.file === "string" ? (best as any).file : null
|
||||||
|
})
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const next = bestDiffFile()
|
||||||
|
if (!next) return
|
||||||
|
const diffs = activeSessionDiffs()
|
||||||
|
if (!Array.isArray(diffs) || diffs.length === 0) return
|
||||||
|
|
||||||
|
const current = selectedFile()
|
||||||
|
if (current && diffs.some((d) => d.file === current)) return
|
||||||
|
setSelectedFile(next)
|
||||||
|
})
|
||||||
|
|
||||||
const normalizeBrowserPath = (input: string) => {
|
const normalizeBrowserPath = (input: string) => {
|
||||||
const raw = String(input || ".").trim()
|
const raw = String(input || ".").trim()
|
||||||
if (!raw || raw === "./") return "."
|
if (!raw || raw === "./") return "."
|
||||||
@@ -1071,6 +1308,11 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
setBrowserSelectedLoading(true)
|
setBrowserSelectedLoading(true)
|
||||||
setBrowserSelectedError(null)
|
setBrowserSelectedError(null)
|
||||||
setBrowserSelectedContent(null)
|
setBrowserSelectedContent(null)
|
||||||
|
|
||||||
|
// Phone: treat file selection as a commit action and close the overlay.
|
||||||
|
if (isPhoneLayout()) {
|
||||||
|
setFilesListOpen(false)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const content = await requestData<FileContent>(browserClient().file.read({ path }), "file.read")
|
const content = await requestData<FileContent>(browserClient().file.read({ path }), "file.read")
|
||||||
const type = (content as any)?.type
|
const type = (content as any)?.type
|
||||||
@@ -1094,7 +1336,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
if (rightPanelTab() !== "browser") return
|
if (rightPanelTab() !== "files") return
|
||||||
if (browserLoading()) return
|
if (browserLoading()) return
|
||||||
if (browserEntries() !== null) return
|
if (browserEntries() !== null) return
|
||||||
void loadBrowserEntries(browserPath())
|
void loadBrowserEntries(browserPath())
|
||||||
@@ -1137,9 +1379,23 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
{ additions: 0, deletions: 0 },
|
{ additions: 0, deletions: 0 },
|
||||||
)
|
)
|
||||||
|
|
||||||
// Select first file by default if none selected
|
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 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])
|
||||||
|
|
||||||
|
// Auto-select the most-changed file if none selected.
|
||||||
const currentSelected = selectedFile()
|
const currentSelected = selectedFile()
|
||||||
const selectedFileData = sorted.find((f) => f.file === currentSelected) || sorted[0]
|
const selectedFileData = sorted.find((f) => f.file === currentSelected) || mostChanged
|
||||||
|
|
||||||
const scopeKey = `${props.instance.id}:${sessionId}`
|
const scopeKey = `${props.instance.id}:${sessionId}`
|
||||||
|
|
||||||
@@ -1153,41 +1409,46 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPhoneLayout()) {
|
|
||||||
return (
|
return (
|
||||||
<div class="files-tab-container">
|
<div class="files-tab-container">
|
||||||
<div class="rounded-lg border border-base bg-surface-secondary p-2 max-h-[32vh] overflow-y-auto">
|
<div class="files-tab-header">
|
||||||
<div class="flex flex-col">
|
<div class="files-tab-header-row">
|
||||||
<For each={sorted}>
|
|
||||||
{(item) => (
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`border-b border-base last:border-b-0 text-left hover:bg-surface-muted rounded-sm ${selectedFileData?.file === item.file ? "bg-surface-base" : ""}`}
|
class="files-toggle-button"
|
||||||
onClick={() => setSelectedFile(item.file)}
|
onClick={() => {
|
||||||
title={item.file}
|
setChangesListTouched(true)
|
||||||
|
setChangesListOpen((current) => {
|
||||||
|
const next = !current
|
||||||
|
persistListOpen("changes", next)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between gap-3">
|
{changesListOpen() ? "Hide files" : "Show files"}
|
||||||
<div
|
|
||||||
class="text-xs font-mono text-primary min-w-0 flex-1 overflow-hidden whitespace-nowrap"
|
|
||||||
title={item.file}
|
|
||||||
style="text-overflow: ellipsis; direction: rtl; text-align: left; unicode-bidi: plaintext;"
|
|
||||||
>
|
|
||||||
{item.file}
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center gap-2 text-[11px] flex-shrink-0">
|
|
||||||
<span style={{ color: "var(--session-status-idle-fg)" }}>{`+${item.additions}`}</span>
|
|
||||||
<span style={{ color: "var(--session-status-working-fg)" }}>{`-${item.deletions}`}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
</For>
|
<span class="files-tab-selected-path" title={selectedFileData?.file || ""}>
|
||||||
|
{selectedFileData?.file || ""}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="files-tab-stats" style={{ "flex": "0 0 auto" }}>
|
||||||
|
<span class="files-tab-stat files-tab-stat-additions">
|
||||||
|
<span class="files-tab-stat-value">+{totals.additions}</span>
|
||||||
|
</span>
|
||||||
|
<span class="files-tab-stat files-tab-stat-deletions">
|
||||||
|
<span class="files-tab-stat-value">-{totals.deletions}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="files-tab-body">
|
||||||
|
<Show
|
||||||
|
when={!isPhoneLayout() && changesListOpen()}
|
||||||
|
fallback={
|
||||||
<div class="file-viewer-panel flex-1">
|
<div class="file-viewer-panel flex-1">
|
||||||
<div class="file-viewer-header">
|
<div class="file-viewer-header">
|
||||||
<span class="file-viewer-title">{t("instanceShell.filesShell.viewerTitle")}</span>
|
|
||||||
<div class="file-viewer-toolbar">
|
<div class="file-viewer-toolbar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1256,41 +1517,21 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
>
|
||||||
return (
|
<div class="files-split" style={{ "--files-pane-width": `${changesSplitWidth()}px` }}>
|
||||||
<div class="files-tab-container">
|
|
||||||
<div class="files-tab-header">
|
|
||||||
<div class="files-tab-stats">
|
|
||||||
<span class="files-tab-stat">
|
|
||||||
<span class="files-tab-stat-value">{sorted.length}</span>
|
|
||||||
<span>files</span>
|
|
||||||
</span>
|
|
||||||
<span class="files-tab-stat files-tab-stat-additions">
|
|
||||||
<span class="files-tab-stat-value">+{totals.additions}</span>
|
|
||||||
<span>additions</span>
|
|
||||||
</span>
|
|
||||||
<span class="files-tab-stat files-tab-stat-deletions">
|
|
||||||
<span class="files-tab-stat-value">-{totals.deletions}</span>
|
|
||||||
<span>deletions</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex min-h-0 gap-3 flex-1">
|
|
||||||
<div class="file-list-panel">
|
<div class="file-list-panel">
|
||||||
<div class="file-list-header">
|
|
||||||
<span class="file-list-title">{t("instanceShell.filesShell.fileListTitle")}</span>
|
|
||||||
<span class="file-list-count">{sorted.length}</span>
|
|
||||||
</div>
|
|
||||||
<div class="file-list-scroll">
|
<div class="file-list-scroll">
|
||||||
<For each={sorted}>
|
<For each={sorted}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div
|
<div
|
||||||
class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`}
|
class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`}
|
||||||
onClick={() => setSelectedFile(item.file)}
|
onClick={() => {
|
||||||
|
setSelectedFile(item.file)
|
||||||
|
if (isPhoneLayout()) {
|
||||||
|
setChangesListOpen(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div class="file-list-item-content">
|
<div class="file-list-item-content">
|
||||||
<div class="file-list-item-path" title={item.file}>
|
<div class="file-list-item-path" title={item.file}>
|
||||||
@@ -1306,9 +1547,16 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="file-split-handle"
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-label="Resize file list"
|
||||||
|
onMouseDown={handleSplitResizeMouseDown("changes")}
|
||||||
|
onTouchStart={handleSplitResizeTouchStart("changes")}
|
||||||
|
/>
|
||||||
<div class="file-viewer-panel flex-1">
|
<div class="file-viewer-panel flex-1">
|
||||||
<div class="file-viewer-header">
|
<div class="file-viewer-header">
|
||||||
<span class="file-viewer-title">{t("instanceShell.filesShell.viewerTitle")}</span>
|
|
||||||
<div class="file-viewer-toolbar">
|
<div class="file-viewer-toolbar">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1378,6 +1626,56 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={isPhoneLayout()}>
|
||||||
|
<Show when={changesListOpen()}>
|
||||||
|
<div class="file-list-overlay" role="dialog" aria-label="Changes">
|
||||||
|
<div class="file-list-overlay-header">
|
||||||
|
<span class="files-tab-selected-path" title={selectedFileData?.file || ""}>
|
||||||
|
{selectedFileData?.file || ""}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="files-toggle-button"
|
||||||
|
onClick={() => {
|
||||||
|
setChangesListTouched(true)
|
||||||
|
setChangesListOpen(false)
|
||||||
|
persistListOpen("changes", false)
|
||||||
|
}}
|
||||||
|
aria-label="Close files"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="file-list-scroll">
|
||||||
|
<For each={sorted}>
|
||||||
|
{(item) => (
|
||||||
|
<div
|
||||||
|
class={`file-list-item ${selectedFileData?.file === item.file ? "file-list-item-active" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedFile(item.file)
|
||||||
|
setChangesListOpen(false)
|
||||||
|
}}
|
||||||
|
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>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1402,12 +1700,30 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const parent = getParentPath(browserPath())
|
const parent = getParentPath(browserPath())
|
||||||
const scopeKey = `${props.instance.id}:${worktreeSlugForViewer()}`
|
const scopeKey = `${props.instance.id}:${worktreeSlugForViewer()}`
|
||||||
|
|
||||||
|
const toggleFilesList = () => {
|
||||||
|
setFilesListTouched(true)
|
||||||
|
setFilesListOpen((current) => {
|
||||||
|
const next = !current
|
||||||
|
persistListOpen("files", next)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerDisplayedPath = () => browserSelectedPath() || browserPath()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="files-tab-container">
|
<div class="files-tab-container">
|
||||||
<div class="files-tab-header">
|
<div class="files-tab-header">
|
||||||
|
<div class="files-tab-header-row">
|
||||||
|
<button type="button" class="files-toggle-button" onClick={toggleFilesList}>
|
||||||
|
{filesListOpen() ? "Hide files" : "Show files"}
|
||||||
|
</button>
|
||||||
|
|
||||||
<div class="files-tab-stats">
|
<div class="files-tab-stats">
|
||||||
<span class="files-tab-stat">
|
<span class="files-tab-stat">
|
||||||
<span class="files-tab-stat-value">{browserPath()}</span>
|
<span class="files-tab-selected-path" title={headerDisplayedPath()}>
|
||||||
|
{headerDisplayedPath()}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<Show when={browserLoading()}>
|
<Show when={browserLoading()}>
|
||||||
<span>Loading…</span>
|
<span>Loading…</span>
|
||||||
@@ -1417,57 +1733,13 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex min-h-0 gap-3 flex-1">
|
|
||||||
<div class="file-list-panel">
|
|
||||||
<div class="file-list-header">
|
|
||||||
<span class="file-list-title">Files</span>
|
|
||||||
<span class="file-list-count">{sorted.length}</span>
|
|
||||||
</div>
|
|
||||||
<div class="file-list-scroll">
|
|
||||||
<Show when={parent}>
|
|
||||||
{(p) => (
|
|
||||||
<div class="file-list-item" onClick={() => void loadBrowserEntries(p())}>
|
|
||||||
<div class="file-list-item-content">
|
|
||||||
<div class="file-list-item-path" title={p()}>
|
|
||||||
..
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Show>
|
|
||||||
|
|
||||||
<For each={sorted}>
|
|
||||||
{(item) => (
|
|
||||||
<div
|
|
||||||
class={`file-list-item ${browserSelectedPath() === item.path ? "file-list-item-active" : ""}`}
|
|
||||||
onClick={() => {
|
|
||||||
if (item.type === "directory") {
|
|
||||||
void loadBrowserEntries(item.path)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
void openBrowserFile(item.path)
|
|
||||||
}}
|
|
||||||
title={item.path}
|
|
||||||
>
|
|
||||||
<div class="file-list-item-content">
|
|
||||||
<div class="file-list-item-path" title={item.path}>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
<div class="file-list-item-stats">
|
|
||||||
<span class="text-[10px] text-secondary">{item.type}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="files-tab-body">
|
||||||
|
<Show
|
||||||
|
when={!isPhoneLayout() && filesListOpen()}
|
||||||
|
fallback={
|
||||||
<div class="file-viewer-panel flex-1">
|
<div class="file-viewer-panel flex-1">
|
||||||
<div class="file-viewer-header">
|
|
||||||
<span class="file-viewer-title">Viewer</span>
|
|
||||||
</div>
|
|
||||||
<div class="file-viewer-content file-viewer-content--monaco">
|
<div class="file-viewer-content file-viewer-content--monaco">
|
||||||
<Show
|
<Show
|
||||||
when={browserSelectedLoading()}
|
when={browserSelectedLoading()}
|
||||||
@@ -1509,6 +1781,164 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="files-split" style={{ "--files-pane-width": `${filesSplitWidth()}px` }}>
|
||||||
|
<div class="file-list-panel">
|
||||||
|
<div class="file-list-scroll">
|
||||||
|
<Show when={parent}>
|
||||||
|
{(p) => (
|
||||||
|
<div class="file-list-item" onClick={() => void loadBrowserEntries(p())}>
|
||||||
|
<div class="file-list-item-content">
|
||||||
|
<div class="file-list-item-path" title={p()}>
|
||||||
|
..
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<For each={sorted}>
|
||||||
|
{(item) => (
|
||||||
|
<div
|
||||||
|
class={`file-list-item ${browserSelectedPath() === item.path ? "file-list-item-active" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (item.type === "directory") {
|
||||||
|
void loadBrowserEntries(item.path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void openBrowserFile(item.path)
|
||||||
|
}}
|
||||||
|
title={item.path}
|
||||||
|
>
|
||||||
|
<div class="file-list-item-content">
|
||||||
|
<div class="file-list-item-path" title={item.path}>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
<div class="file-list-item-stats">
|
||||||
|
<span class="text-[10px] text-secondary">{item.type}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="file-split-handle"
|
||||||
|
role="separator"
|
||||||
|
aria-orientation="vertical"
|
||||||
|
aria-label="Resize file list"
|
||||||
|
onMouseDown={handleSplitResizeMouseDown("files")}
|
||||||
|
onTouchStart={handleSplitResizeTouchStart("files")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="file-viewer-panel flex-1">
|
||||||
|
<div class="file-viewer-content file-viewer-content--monaco">
|
||||||
|
<Show
|
||||||
|
when={browserSelectedLoading()}
|
||||||
|
fallback={
|
||||||
|
<Show
|
||||||
|
when={browserSelectedError()}
|
||||||
|
fallback={
|
||||||
|
<Show
|
||||||
|
when={browserSelectedPath() && browserSelectedContent() !== null
|
||||||
|
? { path: browserSelectedPath() as string, content: browserSelectedContent() as string }
|
||||||
|
: null}
|
||||||
|
fallback={
|
||||||
|
<div class="file-viewer-empty">
|
||||||
|
<span class="file-viewer-empty-text">Select a file to preview</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(payload) => (
|
||||||
|
<MonacoFileViewer
|
||||||
|
scopeKey={scopeKey}
|
||||||
|
path={payload().path}
|
||||||
|
content={payload().content}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{(err) => (
|
||||||
|
<div class="file-viewer-empty">
|
||||||
|
<span class="file-viewer-empty-text">{err()}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="file-viewer-empty">
|
||||||
|
<span class="file-viewer-empty-text">Loading…</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<Show when={isPhoneLayout()}>
|
||||||
|
<Show when={filesListOpen()}>
|
||||||
|
<div class="file-list-overlay" role="dialog" aria-label="Files">
|
||||||
|
<div class="file-list-overlay-header">
|
||||||
|
<span class="files-tab-selected-path" title={browserPath()}>
|
||||||
|
{browserPath()}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="files-toggle-button"
|
||||||
|
onClick={() => {
|
||||||
|
setFilesListTouched(true)
|
||||||
|
setFilesListOpen(false)
|
||||||
|
persistListOpen("files", false)
|
||||||
|
}}
|
||||||
|
aria-label="Close files"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="file-list-scroll">
|
||||||
|
<Show when={parent}>
|
||||||
|
{(p) => (
|
||||||
|
<div class="file-list-item" onClick={() => void loadBrowserEntries(p())}>
|
||||||
|
<div class="file-list-item-content">
|
||||||
|
<div class="file-list-item-path" title={p()}>
|
||||||
|
..
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<For each={sorted}>
|
||||||
|
{(item) => (
|
||||||
|
<div
|
||||||
|
class={`file-list-item ${browserSelectedPath() === item.path ? "file-list-item-active" : ""}`}
|
||||||
|
onClick={() => {
|
||||||
|
if (item.type === "directory") {
|
||||||
|
void loadBrowserEntries(item.path)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
void openBrowserFile(item.path)
|
||||||
|
}}
|
||||||
|
title={item.path}
|
||||||
|
>
|
||||||
|
<div class="file-list-item-content">
|
||||||
|
<div class="file-list-item-path" title={item.path}>
|
||||||
|
{item.name}
|
||||||
|
</div>
|
||||||
|
<div class="file-list-item-stats">
|
||||||
|
<span class="text-[10px] text-secondary">{item.type}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -1555,7 +1985,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
if (file) {
|
if (file) {
|
||||||
setSelectedFile(file)
|
setSelectedFile(file)
|
||||||
}
|
}
|
||||||
setRightPanelTab("files")
|
setRightPanelTab("changes")
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1831,18 +2261,18 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
class={tabClass("files")}
|
class={tabClass("changes")}
|
||||||
aria-selected={rightPanelTab() === "files"}
|
aria-selected={rightPanelTab() === "changes"}
|
||||||
onClick={() => setRightPanelTab("files")}
|
onClick={() => setRightPanelTab("changes")}
|
||||||
>
|
>
|
||||||
<span class="tab-label">{t("instanceShell.rightPanel.tabs.changes")}</span>
|
<span class="tab-label">{t("instanceShell.rightPanel.tabs.changes")}</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
role="tab"
|
role="tab"
|
||||||
class={tabClass("browser")}
|
class={tabClass("files")}
|
||||||
aria-selected={rightPanelTab() === "browser"}
|
aria-selected={rightPanelTab() === "files"}
|
||||||
onClick={() => setRightPanelTab("browser")}
|
onClick={() => setRightPanelTab("files")}
|
||||||
>
|
>
|
||||||
<span class="tab-label">{t("instanceShell.rightPanel.tabs.files")}</span>
|
<span class="tab-label">{t("instanceShell.rightPanel.tabs.files")}</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -1864,8 +2294,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
<Show when={rightPanelTab() === "files"}>{renderFilesTabContent()}</Show>
|
<Show when={rightPanelTab() === "changes"}>{renderFilesTabContent()}</Show>
|
||||||
<Show when={rightPanelTab() === "browser"}>{renderBrowserTabContent()}</Show>
|
<Show when={rightPanelTab() === "files"}>{renderBrowserTabContent()}</Show>
|
||||||
<Show when={rightPanelTab() === "status"}>{renderStatusTabContent()}</Show>
|
<Show when={rightPanelTab() === "status"}>{renderStatusTabContent()}</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -70,18 +70,87 @@
|
|||||||
|
|
||||||
/* Files tab layout */
|
/* Files tab layout */
|
||||||
.files-tab-container {
|
.files-tab-container {
|
||||||
@apply flex flex-col h-full min-h-0 p-3 gap-3;
|
@apply flex flex-col h-full min-h-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Split view (file list + viewer) */
|
||||||
|
.files-split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: var(--files-pane-width, 320px) 10px minmax(0, 1fr);
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-split-handle {
|
||||||
|
cursor: col-resize;
|
||||||
|
background-color: transparent;
|
||||||
|
border-left: 1px solid var(--border-base);
|
||||||
|
border-right: 1px solid var(--border-base);
|
||||||
|
user-select: none;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-split-handle:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.files-tab-header {
|
.files-tab-header {
|
||||||
@apply flex items-center justify-between gap-2 px-1;
|
@apply flex items-center justify-between gap-2 px-3 py-2 border-b;
|
||||||
|
border-color: var(--border-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.files-tab-header-row {
|
||||||
|
@apply flex items-center gap-2 w-full min-w-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-toggle-button {
|
||||||
|
@apply text-[11px] px-2 py-1 border border-base transition-colors;
|
||||||
|
background-color: var(--surface-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-toggle-button:hover {
|
||||||
|
background-color: var(--surface-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.files-tab-body {
|
||||||
|
@apply flex flex-col flex-1 min-h-0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-overlay {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--surface-secondary);
|
||||||
|
border-left: 1px solid var(--border-base);
|
||||||
|
z-index: 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list-overlay-header {
|
||||||
|
@apply flex items-center justify-between gap-2 px-3 py-2 border-b;
|
||||||
|
border-color: var(--border-base);
|
||||||
|
background-color: var(--surface-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Overlay title intentionally unused; header shows current path instead. */
|
||||||
|
|
||||||
.files-tab-stats {
|
.files-tab-stats {
|
||||||
@apply flex items-center gap-3 text-[11px];
|
@apply flex items-center gap-3 text-[11px];
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.files-tab-selected-path {
|
||||||
|
@apply text-xs font-mono min-w-0 flex-1 overflow-hidden whitespace-nowrap;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
direction: rtl;
|
||||||
|
text-align: left;
|
||||||
|
unicode-bidi: plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
.files-tab-stat {
|
.files-tab-stat {
|
||||||
@apply flex items-center gap-1.5;
|
@apply flex items-center gap-1.5;
|
||||||
}
|
}
|
||||||
@@ -100,15 +169,14 @@
|
|||||||
|
|
||||||
/* File list panel */
|
/* File list panel */
|
||||||
.file-list-panel {
|
.file-list-panel {
|
||||||
@apply rounded-lg border flex flex-col min-h-0;
|
@apply flex flex-col min-h-0;
|
||||||
background-color: var(--surface-secondary);
|
background-color: transparent;
|
||||||
border-color: var(--border-base);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-header {
|
.file-list-header {
|
||||||
@apply flex items-center justify-between gap-2 px-3 py-2 border-b;
|
@apply flex items-center justify-between gap-2 px-3 py-2 border-b;
|
||||||
border-color: var(--border-base);
|
border-color: var(--border-base);
|
||||||
background-color: var(--surface-secondary);
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list-title {
|
.file-list-title {
|
||||||
@@ -173,15 +241,14 @@
|
|||||||
|
|
||||||
/* File viewer panel */
|
/* File viewer panel */
|
||||||
.file-viewer-panel {
|
.file-viewer-panel {
|
||||||
@apply rounded-lg border flex flex-col min-h-0;
|
@apply flex flex-col min-h-0;
|
||||||
background-color: var(--surface-secondary);
|
background-color: transparent;
|
||||||
border-color: var(--border-base);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-header {
|
.file-viewer-header {
|
||||||
@apply flex items-center gap-2 px-3 py-2 border-b;
|
@apply flex items-center gap-2 px-3 py-2 border-b;
|
||||||
border-color: var(--border-base);
|
border-color: var(--border-base);
|
||||||
background-color: var(--surface-secondary);
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-viewer-toolbar {
|
.file-viewer-toolbar {
|
||||||
|
|||||||
Reference in New Issue
Block a user