feat(ui): add right panel Changes/Status tabs

This commit is contained in:
Shantur Rathore
2026-02-09 16:12:46 +00:00
parent 8c29741830
commit d143faf8eb
10 changed files with 725 additions and 131 deletions

View File

@@ -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>

View File

@@ -86,6 +86,9 @@ export const instanceMessages = {
"instanceShell.empty.description": "Select a session to view messages",
"instanceShell.rightPanel.title": "Status Panel",
"instanceShell.rightPanel.tabs.changes": "Changes",
"instanceShell.rightPanel.tabs.status": "Status",
"instanceShell.rightPanel.tabs.ariaLabel": "Right panel tabs",
"instanceShell.rightPanel.sections.sessionChanges": "Session Changes",
"instanceShell.rightPanel.sections.plan": "Plan",
"instanceShell.rightPanel.sections.backgroundProcesses": "Background Shells",
@@ -99,6 +102,13 @@ export const instanceMessages = {
"instanceShell.sessionChanges.filesChanged": "{count} files changed",
"instanceShell.sessionChanges.actions.show": "Show changes",
"instanceShell.filesShell.fileListTitle": "File list",
"instanceShell.filesShell.mobileSelectorLabel": "Select file",
"instanceShell.filesShell.mobileSelectorEmpty": "Select a file",
"instanceShell.filesShell.viewerTitle": "Change viewer",
"instanceShell.filesShell.viewerPlaceholder": "Detailed change rendering will be added in the next step.",
"instanceShell.filesShell.viewerEmpty": "No file selected.",
"instanceShell.plan.noSessionSelected": "Select a session to view plan.",
"instanceShell.plan.empty": "Nothing planned yet.",

View File

@@ -86,6 +86,9 @@ export const instanceMessages = {
"instanceShell.empty.description": "Selecciona una sesión para ver mensajes",
"instanceShell.rightPanel.title": "Panel de estado",
"instanceShell.rightPanel.tabs.changes": "Cambios",
"instanceShell.rightPanel.tabs.status": "Estado",
"instanceShell.rightPanel.tabs.ariaLabel": "Pestañas del panel derecho",
"instanceShell.rightPanel.sections.sessionChanges": "Cambios de sesion",
"instanceShell.rightPanel.sections.plan": "Plan",
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en segundo plano",
@@ -99,6 +102,13 @@ export const instanceMessages = {
"instanceShell.sessionChanges.filesChanged": "{count} archivos cambiados",
"instanceShell.sessionChanges.actions.show": "Mostrar cambios",
"instanceShell.filesShell.fileListTitle": "Lista de archivos",
"instanceShell.filesShell.mobileSelectorLabel": "Seleccionar archivo",
"instanceShell.filesShell.mobileSelectorEmpty": "Selecciona un archivo",
"instanceShell.filesShell.viewerTitle": "Visor de cambios",
"instanceShell.filesShell.viewerPlaceholder": "La vista detallada se agregará en el siguiente paso.",
"instanceShell.filesShell.viewerEmpty": "Ningún archivo seleccionado.",
"instanceShell.plan.noSessionSelected": "Selecciona una sesión para ver el plan.",
"instanceShell.plan.empty": "Aún no hay nada planificado.",

View File

@@ -86,6 +86,9 @@ export const instanceMessages = {
"instanceShell.empty.description": "Sélectionnez une session pour voir les messages",
"instanceShell.rightPanel.title": "Panneau d'état",
"instanceShell.rightPanel.tabs.changes": "Modifications",
"instanceShell.rightPanel.tabs.status": "Statut",
"instanceShell.rightPanel.tabs.ariaLabel": "Onglets du panneau droit",
"instanceShell.rightPanel.sections.sessionChanges": "Changements de session",
"instanceShell.rightPanel.sections.plan": "Plan",
"instanceShell.rightPanel.sections.backgroundProcesses": "Shells en arrière-plan",
@@ -99,6 +102,13 @@ export const instanceMessages = {
"instanceShell.sessionChanges.filesChanged": "{count} fichiers modifiés",
"instanceShell.sessionChanges.actions.show": "Afficher les changements",
"instanceShell.filesShell.fileListTitle": "Liste des fichiers",
"instanceShell.filesShell.mobileSelectorLabel": "Sélectionner un fichier",
"instanceShell.filesShell.mobileSelectorEmpty": "Sélectionnez un fichier",
"instanceShell.filesShell.viewerTitle": "Visionneuse de changements",
"instanceShell.filesShell.viewerPlaceholder": "Le rendu détaillé sera ajouté à l'étape suivante.",
"instanceShell.filesShell.viewerEmpty": "Aucun fichier sélectionné.",
"instanceShell.plan.noSessionSelected": "Sélectionnez une session pour voir le plan.",
"instanceShell.plan.empty": "Aucun plan pour l'instant.",

View File

@@ -86,6 +86,9 @@ export const instanceMessages = {
"instanceShell.empty.description": "メッセージを表示するにはセッションを選択してください",
"instanceShell.rightPanel.title": "ステータスパネル",
"instanceShell.rightPanel.tabs.changes": "変更",
"instanceShell.rightPanel.tabs.status": "ステータス",
"instanceShell.rightPanel.tabs.ariaLabel": "右パネルのタブ",
"instanceShell.rightPanel.sections.sessionChanges": "セッション変更",
"instanceShell.rightPanel.sections.plan": "計画",
"instanceShell.rightPanel.sections.backgroundProcesses": "バックグラウンドシェル",
@@ -99,6 +102,13 @@ export const instanceMessages = {
"instanceShell.sessionChanges.filesChanged": "{count} 個のファイルが変更されました",
"instanceShell.sessionChanges.actions.show": "変更を表示",
"instanceShell.filesShell.fileListTitle": "ファイル一覧",
"instanceShell.filesShell.mobileSelectorLabel": "ファイルを選択",
"instanceShell.filesShell.mobileSelectorEmpty": "ファイルを選択してください",
"instanceShell.filesShell.viewerTitle": "変更ビューア",
"instanceShell.filesShell.viewerPlaceholder": "詳細な変更表示は次のステップで追加します。",
"instanceShell.filesShell.viewerEmpty": "ファイルが選択されていません。",
"instanceShell.plan.noSessionSelected": "計画を表示するにはセッションを選択してください。",
"instanceShell.plan.empty": "まだ計画はありません。",

View File

@@ -86,6 +86,9 @@ export const instanceMessages = {
"instanceShell.empty.description": "Выберите сессию, чтобы просмотреть сообщения",
"instanceShell.rightPanel.title": "Панель состояния",
"instanceShell.rightPanel.tabs.changes": "Изменения",
"instanceShell.rightPanel.tabs.status": "Статус",
"instanceShell.rightPanel.tabs.ariaLabel": "Вкладки правой панели",
"instanceShell.rightPanel.sections.sessionChanges": "Изменения сессии",
"instanceShell.rightPanel.sections.plan": "План",
"instanceShell.rightPanel.sections.backgroundProcesses": "Фоновые Shell",
@@ -99,6 +102,13 @@ export const instanceMessages = {
"instanceShell.sessionChanges.filesChanged": "Изменено файлов: {count}",
"instanceShell.sessionChanges.actions.show": "Показать изменения",
"instanceShell.filesShell.fileListTitle": "Список файлов",
"instanceShell.filesShell.mobileSelectorLabel": "Выбрать файл",
"instanceShell.filesShell.mobileSelectorEmpty": "Выберите файл",
"instanceShell.filesShell.viewerTitle": "Просмотр изменений",
"instanceShell.filesShell.viewerPlaceholder": "Подробный рендер изменений будет добавлен на следующем этапе.",
"instanceShell.filesShell.viewerEmpty": "Файл не выбран.",
"instanceShell.plan.noSessionSelected": "Выберите сессию, чтобы просмотреть план.",
"instanceShell.plan.empty": "Пока ничего не запланировано.",

View File

@@ -86,6 +86,9 @@ export const instanceMessages = {
"instanceShell.empty.description": "选择会话以查看消息",
"instanceShell.rightPanel.title": "状态面板",
"instanceShell.rightPanel.tabs.changes": "更改",
"instanceShell.rightPanel.tabs.status": "状态",
"instanceShell.rightPanel.tabs.ariaLabel": "右侧面板标签页",
"instanceShell.rightPanel.sections.sessionChanges": "会话更改",
"instanceShell.rightPanel.sections.plan": "计划",
"instanceShell.rightPanel.sections.backgroundProcesses": "后台 Shell",
@@ -99,6 +102,13 @@ export const instanceMessages = {
"instanceShell.sessionChanges.filesChanged": "已更改 {count} 个文件",
"instanceShell.sessionChanges.actions.show": "显示更改",
"instanceShell.filesShell.fileListTitle": "文件列表",
"instanceShell.filesShell.mobileSelectorLabel": "选择文件",
"instanceShell.filesShell.mobileSelectorEmpty": "请选择文件",
"instanceShell.filesShell.viewerTitle": "更改查看器",
"instanceShell.filesShell.viewerPlaceholder": "详细更改渲染将在下一步中添加。",
"instanceShell.filesShell.viewerEmpty": "未选择文件。",
"instanceShell.plan.noSessionSelected": "选择会话以查看计划。",
"instanceShell.plan.empty": "暂无计划。",

View File

@@ -3,6 +3,7 @@
@import "./panels/modal.css";
@import "./panels/panel-shell.css";
@import "./panels/session-layout.css";
@import "./panels/right-panel.css";
.tab-bar-instance {
@@ -32,11 +33,13 @@
.tab-active {
background-color: var(--tab-active-bg);
color: var(--tab-active-text);
border-bottom: 2px solid var(--accent-primary);
}
.tab-inactive {
background-color: var(--tab-inactive-bg);
color: var(--tab-inactive-text);
border-bottom: 2px solid var(--tab-active-bg);
}
.tab-inactive:hover {

View File

@@ -0,0 +1,358 @@
/* Right Panel / Drawer specific styles */
/* Right panel tab bar - browser-style tabs */
.right-panel-tab-bar {
background-color: var(--surface-secondary);
border-bottom: 1px solid var(--border-base);
position: relative;
}
.right-panel-tab-bar::after {
content: '';
position: absolute;
bottom: -1px;
left: 0;
right: 0;
height: 1px;
background-color: var(--border-base);
z-index: 0;
}
.right-panel-tab-bar .tab-container {
@apply flex items-center justify-between gap-1 px-2 pt-2 pb-0;
}
/* Shortcuts on the left side - match left drawer icon colors */
.tab-strip-shortcuts {
@apply flex items-center gap-1 flex-shrink-0;
color: var(--text-primary);
}
/* Browser-style tabs - using INSTANCE tab color scheme */
.right-panel-tab {
@apply inline-flex items-center gap-2 px-4 py-2 text-xs font-medium transition-all duration-150 relative;
font-family: var(--font-family-sans);
outline: none;
border: 1px solid transparent;
border-bottom: none;
border-radius: 8px 8px 0 0;
margin-right: 2px;
z-index: 1;
}
.right-panel-tab:focus-visible {
@apply ring-2 ring-offset-1;
ring-color: var(--accent-primary);
ring-offset-color: var(--surface-secondary);
}
.right-panel-tab-active {
background-color: var(--tab-active-bg);
border-color: transparent;
border-bottom: 2px solid var(--accent-primary);
color: var(--tab-active-text);
z-index: 2;
}
.right-panel-tab-inactive {
background-color: var(--tab-inactive-bg);
color: var(--tab-inactive-text);
border-color: transparent;
border-bottom: 2px solid var(--tab-active-bg);
}
.right-panel-tab-inactive:hover {
background-color: var(--tab-inactive-hover-bg);
color: var(--text-secondary);
border-color: var(--border-base);
border-bottom-color: transparent;
}
/* Files tab layout */
.files-tab-container {
@apply flex flex-col h-full min-h-0 p-3 gap-3;
}
.files-tab-header {
@apply flex items-center justify-between gap-2 px-1;
}
.files-tab-stats {
@apply flex items-center gap-3 text-[11px];
color: var(--text-muted);
}
.files-tab-stat {
@apply flex items-center gap-1.5;
}
.files-tab-stat-value {
@apply font-semibold;
}
.files-tab-stat-additions {
color: var(--session-status-idle-fg);
}
.files-tab-stat-deletions {
color: var(--session-status-working-fg);
}
/* File list panel */
.file-list-panel {
@apply rounded-lg border flex flex-col min-h-0;
background-color: var(--surface-secondary);
border-color: var(--border-base);
}
.file-list-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);
}
.file-list-title {
@apply text-[11px] font-semibold uppercase tracking-wide;
color: var(--text-muted);
}
.file-list-count {
@apply text-[10px] px-1.5 py-0.5 rounded-full;
background-color: var(--surface-base);
color: var(--text-muted);
border: 1px solid var(--border-base);
}
.file-list-scroll {
@apply flex-1 overflow-y-auto min-h-0;
}
.file-list-item {
@apply px-3 py-2.5 border-b cursor-pointer transition-all duration-150;
border-color: var(--border-base);
background-color: transparent;
}
.file-list-item:last-child {
@apply border-b-0;
}
.file-list-item:hover {
background-color: var(--surface-hover);
}
.file-list-item-active {
background-color: var(--surface-base);
box-shadow: inset 0 0 0 1px var(--accent-primary);
}
.file-list-item-content {
@apply flex items-center justify-between gap-3;
}
.file-list-item-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;
}
.file-list-item-stats {
@apply flex items-center gap-2 text-[11px] flex-shrink-0;
}
.file-list-item-additions {
color: var(--session-status-idle-fg);
}
.file-list-item-deletions {
color: var(--session-status-working-fg);
}
/* File viewer panel */
.file-viewer-panel {
@apply rounded-lg border flex flex-col min-h-0;
background-color: var(--surface-secondary);
border-color: var(--border-base);
}
.file-viewer-header {
@apply flex items-center gap-2 px-3 py-2 border-b;
border-color: var(--border-base);
background-color: var(--surface-secondary);
}
.file-viewer-title {
@apply text-[11px] font-semibold uppercase tracking-wide;
color: var(--text-muted);
}
.file-viewer-content {
@apply flex-1 p-4 overflow-auto min-h-0;
}
.file-viewer-empty {
@apply flex flex-col items-center justify-center h-full gap-3 text-center;
color: var(--text-muted);
}
.file-viewer-empty-icon {
@apply w-8 h-8 opacity-40;
color: var(--text-muted);
}
.file-viewer-empty-text {
@apply text-xs;
}
.file-viewer-placeholder {
@apply text-xs leading-relaxed;
color: var(--text-muted);
}
.file-viewer-selected-file {
@apply flex flex-col gap-2;
}
.file-viewer-file-name {
@apply text-xs font-mono px-2 py-1.5 rounded border;
background-color: var(--surface-base);
border-color: var(--border-base);
color: var(--text-primary);
word-break: break-all;
}
/* Mobile file selector */
.mobile-file-selector {
@apply rounded-lg border flex flex-col gap-3 p-3;
background-color: var(--surface-secondary);
border-color: var(--border-base);
}
.mobile-file-selector-label {
@apply text-[11px] font-semibold uppercase tracking-wide;
color: var(--text-muted);
}
.mobile-file-selector-trigger {
@apply w-full;
}
.mobile-file-viewer {
@apply rounded-lg border flex flex-col min-h-[220px];
background-color: var(--surface-secondary);
border-color: var(--border-base);
}
/* Status tab layout */
.status-tab-container {
@apply flex flex-col h-full min-h-0;
}
.status-tab-context-panel {
@apply border-b;
border-color: var(--border-base);
background-color: var(--surface-secondary);
}
/* Accordion improvements for right panel */
.right-panel-accordion {
@apply flex flex-col;
}
.right-panel-accordion-item {
@apply border-b last:border-b-0;
border-color: var(--border-base);
background-color: var(--surface-secondary);
}
.right-panel-accordion-trigger {
@apply w-full flex items-center justify-between gap-3 px-3 py-2.5 text-[11px] font-semibold uppercase tracking-wide transition-colors duration-150;
color: var(--text-secondary);
background-color: transparent;
}
.right-panel-accordion-trigger:hover {
background-color: var(--surface-hover);
color: var(--text-primary);
}
.right-panel-accordion-chevron {
@apply h-4 w-4 transition-transform duration-200;
color: var(--text-muted);
}
.right-panel-accordion-chevron-expanded {
transform: rotate(180deg);
}
.right-panel-accordion-content {
@apply w-full px-3 pb-3 text-sm;
color: var(--text-primary);
min-height: 0;
}
.right-panel-accordion-content [data-accordion-content] {
min-height: 0;
}
/* Background process cards in status panel */
.status-process-card {
@apply rounded-lg border flex flex-col gap-2 p-3 transition-all duration-150;
background-color: var(--surface-base);
border-color: var(--border-base);
}
.status-process-card:hover {
border-color: var(--border-strong);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.status-process-header {
@apply flex flex-col gap-1;
}
.status-process-title {
@apply text-xs font-semibold;
color: var(--text-primary);
}
.status-process-meta {
@apply flex flex-wrap gap-2 text-[11px];
color: var(--text-muted);
}
.status-process-actions {
@apply grid grid-cols-3 gap-2;
}
/* Empty states */
.right-panel-empty {
@apply flex flex-col items-center justify-center text-center gap-2;
color: var(--text-muted);
}
.right-panel-empty--left {
@apply items-start justify-start text-left w-full;
}
.right-panel-empty-text {
@apply text-xs;
}
/* Dark mode adjustments */
[data-theme="dark"] .right-panel-tab-active {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
[data-theme="dark"] .file-list-item-active {
box-shadow: inset 0 0 0 1px var(--accent-primary), 0 0 0 1px var(--surface-secondary);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) .right-panel-tab-active {
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
}
}

View File

@@ -58,11 +58,13 @@
.tab-active {
background-color: var(--tab-active-bg);
color: var(--tab-active-text);
border-bottom: 2px solid var(--accent-primary);
}
.tab-inactive {
background-color: var(--tab-inactive-bg);
color: var(--tab-inactive-text);
border-bottom: 2px solid var(--tab-active-bg);
}
.tab-inactive:hover {