From d143faf8eb1b9b7b4d716198e121e41568aa9e2a Mon Sep 17 00:00:00 2001 From: Shantur Rathore Date: Mon, 9 Feb 2026 16:12:46 +0000 Subject: [PATCH] feat(ui): add right panel Changes/Status tabs --- .../components/instance/instance-shell2.tsx | 433 ++++++++++++------ .../ui/src/lib/i18n/messages/en/instance.ts | 10 + .../ui/src/lib/i18n/messages/es/instance.ts | 10 + .../ui/src/lib/i18n/messages/fr/instance.ts | 10 + .../ui/src/lib/i18n/messages/ja/instance.ts | 10 + .../ui/src/lib/i18n/messages/ru/instance.ts | 10 + .../src/lib/i18n/messages/zh-Hans/instance.ts | 10 + packages/ui/src/styles/panels.css | 3 + packages/ui/src/styles/panels/right-panel.css | 358 +++++++++++++++ packages/ui/src/styles/panels/tabs.css | 2 + 10 files changed, 725 insertions(+), 131 deletions(-) create mode 100644 packages/ui/src/styles/panels/right-panel.css diff --git a/packages/ui/src/components/instance/instance-shell2.tsx b/packages/ui/src/components/instance/instance-shell2.tsx index 5e2ed131..bd977dff 100644 --- a/packages/ui/src/components/instance/instance-shell2.tsx +++ b/packages/ui/src/components/instance/instance-shell2.tsx @@ -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 = (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 = (props) => { const [activeResizeSide, setActiveResizeSide] = createSignal<"left" | "right" | null>(null) const [resizeStartX, setResizeStartX] = createSignal(0) const [resizeStartWidth, setResizeStartWidth] = createSignal(0) + const [rightPanelTab, setRightPanelTab] = createSignal(readStoredRightPanelTab("files")) const [rightPanelExpandedItems, setRightPanelExpandedItems] = createSignal([ "plan", "background-processes", @@ -148,6 +163,7 @@ const InstanceShell2: Component = (props) => { "lsp", "plugins", ]) + const [selectedFile, setSelectedFile] = createSignal(null) const [selectedBackgroundProcess, setSelectedBackgroundProcess] = createSignal(null) const [showBackgroundOutput, setShowBackgroundOutput] = createSignal(false) const [permissionModalOpen, setPermissionModalOpen] = createSignal(false) @@ -168,7 +184,7 @@ const InstanceShell2: Component = (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 = (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 = (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 = (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 = (props) => { ) const RightDrawerContent = () => { - const renderSessionChanges = () => { + const renderFilesTabContent = () => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info") { - return

{t("instanceShell.sessionChanges.noSessionSelected")}

+ return ( +
+ {t("instanceShell.sessionChanges.noSessionSelected")} +
+ ) } const diffs = activeSessionDiffs() if (diffs === undefined) { - return

{t("instanceShell.sessionChanges.loading")}

+ return ( +
+ {t("instanceShell.sessionChanges.loading")} +
+ ) } if (!Array.isArray(diffs) || diffs.length === 0) { - return

{t("instanceShell.sessionChanges.empty")}

+ return ( +
+ {t("instanceShell.sessionChanges.empty")} +
+ ) } const sorted = [...diffs].sort((a, b) => String(a.file || "").localeCompare(String(b.file || ""))) @@ -990,47 +1030,119 @@ const InstanceShell2: Component = (props) => { { additions: 0, deletions: 0 }, ) - return ( -
-
- {t("instanceShell.sessionChanges.filesChanged", { count: sorted.length })} - - {`+${totals.additions}`} - {`-${totals.deletions}`} - -
+ // Select first file by default if none selected + const currentSelected = selectedFile() + const selectedFileData = sorted.find((f) => f.file === currentSelected) || sorted[0] -
-
- - {(item) => ( -
-
-
- {item.file} -
-
- {`+${item.additions}`} - {`-${item.deletions}`} -
+ if (isPhoneLayout()) { + return ( +
+
+ {t("instanceShell.filesShell.mobileSelectorLabel")} + +
+
+
+ {t("instanceShell.filesShell.viewerTitle")} +
+
+ + {t("instanceShell.filesShell.viewerEmpty")}
-
- )} - + } + > + {(file) => ( +
+ {file().file} +

{t("instanceShell.filesShell.viewerPlaceholder")}

+
+ )} + +
+
+
+ ) + } + + return ( +
+
+
+ + {sorted.length} + files + + + +{totals.additions} + additions + + + -{totals.deletions} + deletions +
- +
+
+
+ {t("instanceShell.filesShell.fileListTitle")} + {sorted.length} +
+
+ + {(item) => ( +
setSelectedFile(item.file)} + > +
+
+ {item.file} +
+
+ +{item.additions} + -{item.deletions} +
+
+
+ )} +
+
+
+
+
+ {t("instanceShell.filesShell.viewerTitle")} +
+
+ + {t("instanceShell.filesShell.viewerEmpty")} +
+ } + > + {(file) => ( +
+ {file().file} +

{t("instanceShell.filesShell.viewerPlaceholder")}

+
+ )} + +
+
+
) } @@ -1038,11 +1150,19 @@ const InstanceShell2: Component = (props) => { const renderPlanSectionContent = () => { const sessionId = activeSessionIdForInstance() if (!sessionId || sessionId === "info") { - return

{t("instanceShell.plan.noSessionSelected")}

+ return ( +
+ {t("instanceShell.plan.noSessionSelected")} +
+ ) } const todoState = latestTodoState() if (!todoState) { - return

{t("instanceShell.plan.empty")}

+ return ( +
+ {t("instanceShell.plan.empty")} +
+ ) } return } @@ -1050,17 +1170,21 @@ const InstanceShell2: Component = (props) => { const renderBackgroundProcesses = () => { const processes = backgroundProcessList() if (processes.length === 0) { - return

{t("instanceShell.backgroundProcesses.empty")}

+ return ( +
+ {t("instanceShell.backgroundProcesses.empty")} +
+ ) } return (
{(process) => ( -
-
- {process.title} -
+
+
+ {process.title} +
{t("instanceShell.backgroundProcesses.status", { status: process.status })} @@ -1071,7 +1195,7 @@ const InstanceShell2: Component = (props) => {
-
+
+ +
+ +
+
- - {(activeSession) => ( - - )} -
+
- - - {(section) => ( - - - - {t(section.labelKey)} - - - - - {section.render()} - - - )} - - + {renderFilesTabContent()} + {renderStatusTabContent()}
) @@ -1305,6 +1458,15 @@ const InstanceShell2: Component = (props) => { }, }} > + +