feat(ui): add right panel Changes/Status tabs
This commit is contained in:
@@ -93,20 +93,26 @@ const MIN_SESSION_SIDEBAR_WIDTH = 220
|
||||
const MAX_SESSION_SIDEBAR_WIDTH = 400
|
||||
const RIGHT_DRAWER_WIDTH = 260
|
||||
const MIN_RIGHT_DRAWER_WIDTH = 200
|
||||
const MAX_RIGHT_DRAWER_WIDTH = 380
|
||||
const MAX_RIGHT_DRAWER_WIDTH = 1200
|
||||
const SESSION_CACHE_LIMIT = 5
|
||||
const LEFT_DRAWER_STORAGE_KEY = "opencode-session-sidebar-width-v8"
|
||||
const RIGHT_DRAWER_STORAGE_KEY = "opencode-session-right-drawer-width-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_PANEL_TAB_STORAGE_KEY = "opencode-session-right-panel-tab-v1"
|
||||
|
||||
|
||||
|
||||
|
||||
type LayoutMode = "desktop" | "tablet" | "phone"
|
||||
type RightPanelTab = "files" | "status"
|
||||
|
||||
const clampWidth = (value: number) => Math.min(MAX_SESSION_SIDEBAR_WIDTH, Math.max(MIN_SESSION_SIDEBAR_WIDTH, value))
|
||||
const clampRightWidth = (value: number) => Math.min(MAX_RIGHT_DRAWER_WIDTH, Math.max(MIN_RIGHT_DRAWER_WIDTH, value))
|
||||
const clampRightWidth = (value: number) => {
|
||||
const windowMax = typeof window !== "undefined" ? Math.floor(window.innerWidth * 0.7) : MAX_RIGHT_DRAWER_WIDTH
|
||||
const max = Math.max(MIN_RIGHT_DRAWER_WIDTH, windowMax)
|
||||
return Math.min(max, Math.max(MIN_RIGHT_DRAWER_WIDTH, value))
|
||||
}
|
||||
const getPinStorageKey = (side: "left" | "right") => (side === "left" ? LEFT_PIN_STORAGE_KEY : RIGHT_PIN_STORAGE_KEY)
|
||||
function readStoredPinState(side: "left" | "right", defaultValue: boolean) {
|
||||
if (typeof window === "undefined") return defaultValue
|
||||
@@ -120,11 +126,19 @@ function persistPinState(side: "left" | "right", value: boolean) {
|
||||
window.localStorage.setItem(getPinStorageKey(side), value ? "true" : "false")
|
||||
}
|
||||
|
||||
function readStoredRightPanelTab(defaultValue: RightPanelTab): RightPanelTab {
|
||||
if (typeof window === "undefined") return defaultValue
|
||||
const stored = window.localStorage.getItem(RIGHT_PANEL_TAB_STORAGE_KEY)
|
||||
return stored === "status" ? "status" : defaultValue
|
||||
}
|
||||
|
||||
const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
|
||||
const [sessionSidebarWidth, setSessionSidebarWidth] = createSignal(DEFAULT_SESSION_SIDEBAR_WIDTH)
|
||||
const [rightDrawerWidth, setRightDrawerWidth] = createSignal(RIGHT_DRAWER_WIDTH)
|
||||
const [rightDrawerWidth, setRightDrawerWidth] = createSignal(
|
||||
typeof window !== "undefined" ? clampRightWidth(window.innerWidth * 0.35) : RIGHT_DRAWER_WIDTH,
|
||||
)
|
||||
const [leftPinned, setLeftPinned] = createSignal(true)
|
||||
const [leftOpen, setLeftOpen] = createSignal(true)
|
||||
const [rightPinned, setRightPinned] = createSignal(true)
|
||||
@@ -141,6 +155,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | null>(null)
|
||||
const [resizeStartX, setResizeStartX] = createSignal(0)
|
||||
const [resizeStartWidth, setResizeStartWidth] = createSignal(0)
|
||||
const [rightPanelTab, setRightPanelTab] = createSignal<RightPanelTab>(readStoredRightPanelTab("files"))
|
||||
const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal<string[]>([
|
||||
"plan",
|
||||
"background-processes",
|
||||
@@ -148,6 +163,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
"lsp",
|
||||
"plugins",
|
||||
])
|
||||
const [selectedFile, setSelectedFile] = createSignal<string | null>(null)
|
||||
const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal<BackgroundProcess | null>(null)
|
||||
const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false)
|
||||
const [permissionModalOpen, setPermissionModalOpen] = createSignal(false)
|
||||
@@ -168,7 +184,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
})
|
||||
|
||||
const isPhoneLayout = createMemo(() => layoutMode() === "phone")
|
||||
const leftPinningSupported = createMemo(() => layoutMode() === "desktop")
|
||||
const leftPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||
const rightPinningSupported = createMemo(() => layoutMode() !== "phone")
|
||||
|
||||
const persistPinIfSupported = (side: "left" | "right", value: boolean) => {
|
||||
@@ -196,11 +212,10 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
break
|
||||
}
|
||||
case "tablet": {
|
||||
const rightSaved = readStoredPinState("right", true)
|
||||
setLeftPinned(false)
|
||||
setLeftOpen(false)
|
||||
setRightPinned(rightSaved)
|
||||
setRightOpen(rightSaved)
|
||||
setLeftPinned(true)
|
||||
setLeftOpen(true)
|
||||
setRightPinned(false)
|
||||
setRightOpen(false)
|
||||
break
|
||||
}
|
||||
default:
|
||||
@@ -232,17 +247,25 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
let didLoadRightWidth = false
|
||||
const savedRight = window.localStorage.getItem(RIGHT_DRAWER_STORAGE_KEY)
|
||||
if (savedRight) {
|
||||
const parsed = Number.parseInt(savedRight, 10)
|
||||
if (Number.isFinite(parsed)) {
|
||||
setRightDrawerWidth(clampRightWidth(parsed))
|
||||
didLoadRightWidth = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!didLoadRightWidth) {
|
||||
setRightDrawerWidth(clampRightWidth(window.innerWidth * 0.35))
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
const width = clampWidth(window.innerWidth * 0.3)
|
||||
setSessionSidebarWidth((current) => clampWidth(current || width))
|
||||
const fallbackRight = window.innerWidth * 0.35
|
||||
setRightDrawerWidth((current) => clampRightWidth(current || fallbackRight))
|
||||
measureDrawerHost()
|
||||
}
|
||||
|
||||
@@ -272,6 +295,11 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
window.localStorage.setItem(RIGHT_DRAWER_STORAGE_KEY, rightDrawerWidth().toString())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return
|
||||
window.localStorage.setItem(RIGHT_PANEL_TAB_STORAGE_KEY, rightPanelTab())
|
||||
})
|
||||
|
||||
createEffect(() => {
|
||||
props.tabBarOffset
|
||||
requestAnimationFrame(() => measureDrawerHost())
|
||||
@@ -965,19 +993,31 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
)
|
||||
|
||||
const RightDrawerContent = () => {
|
||||
const renderSessionChanges = () => {
|
||||
const renderFilesTabContent = () => {
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
if (!sessionId || sessionId === "info") {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.sessionChanges.noSessionSelected")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty">
|
||||
<span class="text-xs">{t("instanceShell.sessionChanges.noSessionSelected")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const diffs = activeSessionDiffs()
|
||||
if (diffs === undefined) {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.sessionChanges.loading")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty">
|
||||
<span class="text-xs">{t("instanceShell.sessionChanges.loading")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!Array.isArray(diffs) || diffs.length === 0) {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.sessionChanges.empty")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty">
|
||||
<span class="text-xs">{t("instanceShell.sessionChanges.empty")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const sorted = [...diffs].sort((a, b) => String(a.file || "").localeCompare(String(b.file || "")))
|
||||
@@ -990,47 +1030,119 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
{ additions: 0, deletions: 0 },
|
||||
)
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-3 min-h-0">
|
||||
<div class="flex items-center justify-between gap-2 text-[11px] text-secondary">
|
||||
<span>{t("instanceShell.sessionChanges.filesChanged", { count: sorted.length })}</span>
|
||||
<span class="flex items-center gap-2">
|
||||
<span style={{ color: "var(--session-status-idle-fg)" }}>{`+${totals.additions}`}</span>
|
||||
<span style={{ color: "var(--session-status-working-fg)" }}>{`-${totals.deletions}`}</span>
|
||||
</span>
|
||||
</div>
|
||||
// Select first file by default if none selected
|
||||
const currentSelected = selectedFile()
|
||||
const selectedFileData = sorted.find((f) => f.file === currentSelected) || sorted[0]
|
||||
|
||||
<div class="rounded-md border border-base bg-surface-secondary p-2 max-h-[40vh] overflow-y-auto">
|
||||
<div class="flex flex-col">
|
||||
<For each={sorted}>
|
||||
{(item) => (
|
||||
<div class="py-2 border-b border-base last:border-b-0">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<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>
|
||||
if (isPhoneLayout()) {
|
||||
return (
|
||||
<div class="files-tab-container">
|
||||
<div class="mobile-file-selector">
|
||||
<span class="mobile-file-selector-label">{t("instanceShell.filesShell.mobileSelectorLabel")}</span>
|
||||
<button type="button" class="selector-trigger mobile-file-selector-trigger" disabled>
|
||||
<span class="selector-trigger-label selector-trigger-primary selector-trigger-primary--align-left truncate">
|
||||
{selectedFileData?.file || t("instanceShell.filesShell.mobileSelectorEmpty")}
|
||||
</span>
|
||||
<span class="selector-trigger-icon" aria-hidden="true">
|
||||
<ChevronDown class="w-3 h-3" />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mobile-file-viewer">
|
||||
<div class="file-viewer-header">
|
||||
<span class="file-viewer-title">{t("instanceShell.filesShell.viewerTitle")}</span>
|
||||
</div>
|
||||
<div class="file-viewer-content">
|
||||
<Show
|
||||
when={selectedFileData}
|
||||
fallback={
|
||||
<div class="file-viewer-empty">
|
||||
<span class="file-viewer-empty-text">{t("instanceShell.filesShell.viewerEmpty")}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
}
|
||||
>
|
||||
{(file) => (
|
||||
<div class="file-viewer-selected-file">
|
||||
<span class="file-viewer-file-name">{file().file}</span>
|
||||
<p class="file-viewer-placeholder">{t("instanceShell.filesShell.viewerPlaceholder")}</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="button-tertiary w-full p-1.5 inline-flex items-center justify-center"
|
||||
onClick={() => undefined}
|
||||
>
|
||||
{t("instanceShell.sessionChanges.actions.show")}
|
||||
</button>
|
||||
<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">{t("instanceShell.filesShell.fileListTitle")}</span>
|
||||
<span class="file-list-count">{sorted.length}</span>
|
||||
</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)}
|
||||
>
|
||||
<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>
|
||||
<div class="file-viewer-panel flex-1">
|
||||
<div class="file-viewer-header">
|
||||
<span class="file-viewer-title">{t("instanceShell.filesShell.viewerTitle")}</span>
|
||||
</div>
|
||||
<div class="file-viewer-content">
|
||||
<Show
|
||||
when={selectedFileData}
|
||||
fallback={
|
||||
<div class="file-viewer-empty">
|
||||
<span class="file-viewer-empty-text">{t("instanceShell.filesShell.viewerEmpty")}</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{(file) => (
|
||||
<div class="file-viewer-selected-file">
|
||||
<span class="file-viewer-file-name">{file().file}</span>
|
||||
<p class="file-viewer-placeholder">{t("instanceShell.filesShell.viewerPlaceholder")}</p>
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1038,11 +1150,19 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const renderPlanSectionContent = () => {
|
||||
const sessionId = activeSessionIdForInstance()
|
||||
if (!sessionId || sessionId === "info") {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.plan.noSessionSelected")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty right-panel-empty--left">
|
||||
<span class="text-xs">{t("instanceShell.plan.noSessionSelected")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const todoState = latestTodoState()
|
||||
if (!todoState) {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.plan.empty")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty right-panel-empty--left">
|
||||
<span class="text-xs">{t("instanceShell.plan.empty")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return <TodoListView state={todoState} emptyLabel={t("instanceShell.plan.empty")} showStatusLabel={false} />
|
||||
}
|
||||
@@ -1050,17 +1170,21 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
const renderBackgroundProcesses = () => {
|
||||
const processes = backgroundProcessList()
|
||||
if (processes.length === 0) {
|
||||
return <p class="text-xs text-secondary">{t("instanceShell.backgroundProcesses.empty")}</p>
|
||||
return (
|
||||
<div class="right-panel-empty right-panel-empty--left">
|
||||
<span class="text-xs">{t("instanceShell.backgroundProcesses.empty")}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<For each={processes}>
|
||||
{(process) => (
|
||||
<div class="rounded-md border border-base bg-surface-secondary p-2 flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-1">
|
||||
<span class="text-xs font-semibold text-primary">{process.title}</span>
|
||||
<div class="flex flex-wrap gap-2 text-[11px] text-secondary">
|
||||
<div class="status-process-card">
|
||||
<div class="status-process-header">
|
||||
<span class="status-process-title">{process.title}</span>
|
||||
<div class="status-process-meta">
|
||||
<span>{t("instanceShell.backgroundProcesses.status", { status: process.status })}</span>
|
||||
<Show when={typeof process.outputSizeBytes === "number"}>
|
||||
<span>
|
||||
@@ -1071,7 +1195,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="status-process-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="button-tertiary w-full p-1 inline-flex items-center justify-center"
|
||||
@@ -1108,12 +1232,7 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: "session-changes",
|
||||
labelKey: "instanceShell.rightPanel.sections.sessionChanges",
|
||||
render: renderSessionChanges,
|
||||
},
|
||||
const statusSections = [
|
||||
{
|
||||
id: "plan",
|
||||
labelKey: "instanceShell.rightPanel.sections.plan",
|
||||
@@ -1164,8 +1283,8 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
|
||||
createEffect(() => {
|
||||
const currentExpanded = new Set(rightPanelExpandedItems())
|
||||
if (sections.every((section) => currentExpanded.has(section.id))) return
|
||||
setRightPanelExpandedItems(sections.map((section) => section.id))
|
||||
if (statusSections.every((section) => currentExpanded.has(section.id))) return
|
||||
setRightPanelExpandedItems(statusSections.map((section) => section.id))
|
||||
})
|
||||
|
||||
const handleAccordionChange = (values: string[]) => {
|
||||
@@ -1174,78 +1293,112 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
|
||||
const isSectionExpanded = (id: string) => rightPanelExpandedItems().includes(id)
|
||||
|
||||
const renderStatusTabContent = () => (
|
||||
<div class="status-tab-container">
|
||||
<Show when={activeSessionForInstance()}>
|
||||
{(activeSession) => (
|
||||
<ContextUsagePanel
|
||||
instanceId={props.instance.id}
|
||||
sessionId={activeSession().id}
|
||||
class="status-tab-context-panel"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Accordion.Root
|
||||
class="right-panel-accordion"
|
||||
collapsible
|
||||
multiple
|
||||
value={rightPanelExpandedItems()}
|
||||
onChange={handleAccordionChange}
|
||||
>
|
||||
<For each={statusSections}>
|
||||
{(section) => (
|
||||
<Accordion.Item
|
||||
value={section.id}
|
||||
class="right-panel-accordion-item"
|
||||
>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger class="right-panel-accordion-trigger">
|
||||
<span>{t(section.labelKey)}</span>
|
||||
<ChevronDown
|
||||
class={`right-panel-accordion-chevron ${isSectionExpanded(section.id) ? "right-panel-accordion-chevron-expanded" : ""}`}
|
||||
/>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="right-panel-accordion-content">
|
||||
{section.render()}
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)}
|
||||
</For>
|
||||
</Accordion.Root>
|
||||
</div>
|
||||
)
|
||||
|
||||
const tabClass = (tab: RightPanelTab) =>
|
||||
`right-panel-tab ${rightPanelTab() === tab ? "right-panel-tab-active" : "right-panel-tab-inactive"}`
|
||||
|
||||
return (
|
||||
<div class="flex flex-col h-full" ref={setRightDrawerContentEl}>
|
||||
<div class="border-b border-base text-primary">
|
||||
<div class="relative flex items-center px-4 py-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={rightDrawerState() === "floating-open"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={t("instanceShell.rightDrawer.toggle.close")}
|
||||
title={t("instanceShell.rightDrawer.toggle.close")}
|
||||
onClick={closeRightDrawer}
|
||||
>
|
||||
<MenuOpenIcon fontSize="small" sx={{ transform: "scaleX(-1)" }} />
|
||||
</IconButton>
|
||||
</Show>
|
||||
<Show when={!isPhoneLayout()}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={rightPinned() ? t("instanceShell.rightDrawer.unpin") : t("instanceShell.rightDrawer.pin")}
|
||||
onClick={() => (rightPinned() ? unpinRightDrawer() : pinRightDrawer())}
|
||||
>
|
||||
{rightPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<span class="session-sidebar-title text-sm font-semibold uppercase text-primary">
|
||||
{t("instanceShell.rightPanel.title")}
|
||||
</span>
|
||||
<div class="right-panel-tab-bar">
|
||||
<div class="tab-container">
|
||||
<div class="tab-scroll">
|
||||
<div class="tab-strip">
|
||||
<div class="tab-strip-shortcuts text-primary">
|
||||
<Show when={rightDrawerState() === "floating-open"}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={t("instanceShell.rightDrawer.toggle.close")}
|
||||
title={t("instanceShell.rightDrawer.toggle.close")}
|
||||
onClick={closeRightDrawer}
|
||||
>
|
||||
<MenuOpenIcon fontSize="small" sx={{ transform: "scaleX(-1)" }} />
|
||||
</IconButton>
|
||||
</Show>
|
||||
<Show when={!isPhoneLayout()}>
|
||||
<IconButton
|
||||
size="small"
|
||||
color="inherit"
|
||||
aria-label={rightPinned() ? t("instanceShell.rightDrawer.unpin") : t("instanceShell.rightDrawer.pin")}
|
||||
onClick={() => (rightPinned() ? unpinRightDrawer() : pinRightDrawer())}
|
||||
>
|
||||
{rightPinned() ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="tab-strip-tabs" role="tablist" aria-label={t("instanceShell.rightPanel.tabs.ariaLabel")}>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={tabClass("files")}
|
||||
aria-selected={rightPanelTab() === "files"}
|
||||
onClick={() => setRightPanelTab("files")}
|
||||
>
|
||||
<span class="tab-label">{t("instanceShell.rightPanel.tabs.changes")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
class={tabClass("status")}
|
||||
aria-selected={rightPanelTab() === "status"}
|
||||
onClick={() => setRightPanelTab("status")}
|
||||
>
|
||||
<span class="tab-label">{t("instanceShell.rightPanel.tabs.status")}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="tab-strip-spacer" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={activeSessionForInstance()}>
|
||||
{(activeSession) => (
|
||||
<ContextUsagePanel
|
||||
instanceId={props.instance.id}
|
||||
sessionId={activeSession().id}
|
||||
class="border-t border-base"
|
||||
/>
|
||||
)}
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<Accordion.Root
|
||||
class="flex flex-col"
|
||||
collapsible
|
||||
multiple
|
||||
value={rightPanelExpandedItems()}
|
||||
onChange={handleAccordionChange}
|
||||
>
|
||||
<For each={sections}>
|
||||
{(section) => (
|
||||
<Accordion.Item
|
||||
value={section.id}
|
||||
class="w-full border border-base bg-surface-secondary text-primary"
|
||||
>
|
||||
<Accordion.Header>
|
||||
<Accordion.Trigger class="w-full flex items-center justify-between gap-3 px-3 py-2 text-[11px] font-semibold uppercase tracking-wide">
|
||||
<span>{t(section.labelKey)}</span>
|
||||
<ChevronDown
|
||||
class={`h-4 w-4 transition-transform duration-150 ${isSectionExpanded(section.id) ? "rotate-180" : ""}`}
|
||||
/>
|
||||
</Accordion.Trigger>
|
||||
</Accordion.Header>
|
||||
<Accordion.Content class="w-full px-3 pb-3 text-sm text-primary">
|
||||
{section.render()}
|
||||
</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
)}
|
||||
</For>
|
||||
</Accordion.Root>
|
||||
<Show when={rightPanelTab() === "files"}>{renderFilesTabContent()}</Show>
|
||||
<Show when={rightPanelTab() === "status"}>{renderStatusTabContent()}</Show>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -1305,6 +1458,15 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Show when={!isPhoneLayout()}>
|
||||
<div
|
||||
class="session-resize-handle session-resize-handle--left"
|
||||
onMouseDown={handleDrawerResizeMouseDown("left")}
|
||||
onTouchStart={handleDrawerResizeTouchStart("left")}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Show>
|
||||
<LeftDrawerContent />
|
||||
</Drawer>
|
||||
)
|
||||
@@ -1364,6 +1526,15 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Show when={!isPhoneLayout()}>
|
||||
<div
|
||||
class="session-resize-handle session-resize-handle--right"
|
||||
onMouseDown={handleDrawerResizeMouseDown("right")}
|
||||
onTouchStart={handleDrawerResizeTouchStart("right")}
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Show>
|
||||
<RightDrawerContent />
|
||||
</Drawer>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user