From c26359917f8ffa449d20e7e2eab773dfea2f9c97 Mon Sep 17 00:00:00 2001 From: spellitwithaph Date: Wed, 27 May 2026 10:10:17 -0400 Subject: [PATCH] feat: add collapse/expand toggle for workspace panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a chevron-right collapse button after the last tab (Files, Git, Terminal, Info) in the workspace panel header. Clicking it hides the workspace panel and lets the main chat column fill the full width. When collapsed, an expand strip with a chevron-left button appears below the context bar to re-open the workspace panel. Changes: - Add workspacePanelCollapsed boolean to AppState - Add collapse button to WorkspacePanel header tabs - Add expand strip with toggle button in main column when collapsed - Adjust grid layout to hide panel and stretch main when collapsed - Handle collapsed state at ≤1180px responsive breakpoint --- src/client/src/appState.ts | 2 ++ src/client/src/components/PiWebApp.ts | 9 +++++++-- src/client/src/components/WorkspacePanel.ts | 7 +++++++ src/client/src/components/shared.ts | 11 ++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/client/src/appState.ts b/src/client/src/appState.ts index 409bf73..17776d7 100644 --- a/src/client/src/appState.ts +++ b/src/client/src/appState.ts @@ -45,6 +45,7 @@ export interface AppState { activeTerminalCount: number; selectedTerminalId: string | undefined; piWebStatus: PiWebStatusResponse | undefined; + workspacePanelCollapsed: boolean; error: string; } @@ -133,6 +134,7 @@ export function initialAppState(): AppState { activeTerminalCount: 0, selectedTerminalId: undefined, piWebStatus: undefined, + workspacePanelCollapsed: false, error: "", }; } diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts index 564f86c..fd4a1fa 100644 --- a/src/client/src/components/PiWebApp.ts +++ b/src/client/src/components/PiWebApp.ts @@ -511,7 +511,11 @@ export class PiWebApp extends LitElement { const panelContext = workspace === undefined ? undefined : this.createWorkspacePanelContext(workspace); const workspaceLabelItems = workspace === undefined ? [] : this.plugins.getWorkspaceLabelItems(this.state, workspace); const emptyState = workspace === undefined ? this.workspacePanelEmptyState() : undefined; - return html` { this.openWorkspaceTool(tool); }}>`; + return html` { this.openWorkspaceTool(tool); }} .onToggleCollapse=${() => { this.toggleWorkspacePanelCollapse(); }}>`; + } + + private toggleWorkspacePanelCollapse(): void { + this.setState({ workspacePanelCollapsed: !this.state.workspacePanelCollapsed }); } private renderNavigationPanel(autoSwitchToChat: boolean) { @@ -1192,10 +1196,11 @@ export class PiWebApp extends LitElement { override render() { const state = this.state; return html` -
+
${this.renderContextBar()} + ${state.workspacePanelCollapsed ? html`
` : null}
diff --git a/src/client/src/components/WorkspacePanel.ts b/src/client/src/components/WorkspacePanel.ts index 826b293..11d5e75 100644 --- a/src/client/src/components/WorkspacePanel.ts +++ b/src/client/src/components/WorkspacePanel.ts @@ -19,7 +19,9 @@ export class WorkspacePanel extends LitElement { @property({ attribute: false }) panels: QualifiedWorkspacePanelContribution[] = []; @property({ attribute: false }) workspaceLabelItems: WorkspaceLabelItem[] = []; @property({ type: Boolean }) hideToolTabs = false; + @property({ type: Boolean }) collapsed = false; @property({ attribute: false }) onSelectTool: (tool: QualifiedContributionId) => void = () => undefined; + @property({ attribute: false }) onToggleCollapse: () => void = () => undefined; @query(".workspace-header-strip") private workspaceHeaderStrip?: HTMLElement | null; @state() private workspaceHeaderCanScrollLeft = false; @state() private workspaceHeaderCanScrollRight = false; @@ -69,6 +71,7 @@ export class WorkspacePanel extends LitElement { ${visiblePanels.map((panel) => html` `)} +
`} ${renderWorkspaceLabel(workspace.label, this.workspaceLabelItems, workspace.path)} @@ -92,6 +95,10 @@ export class WorkspacePanel extends LitElement { return html`${panel.title} ${badge}`; } + private renderCollapseIcon(): TemplateResult { + return html``; + } + private renderEmptyState(state: WorkspacePanelEmptyState): TemplateResult { return html`
diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts index 41e06be..118a9b8 100644 --- a/src/client/src/components/shared.ts +++ b/src/client/src/components/shared.ts @@ -52,6 +52,8 @@ export interface CompletionItem { export const appStyles = css` :host { display: block; height: 100dvh; box-sizing: border-box; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); color: var(--pi-text); background: var(--pi-bg); font: 14px system-ui, sans-serif; } .shell { display: grid; grid-template-columns: 340px minmax(420px, 1fr) minmax(360px, 42vw); height: 100%; min-height: 0; } + .shell.workspace-panel-collapsed { grid-template-columns: 340px minmax(420px, 1fr); } + .shell.workspace-panel-collapsed > workspace-panel { display: none; } aside { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--pi-border); overflow: hidden; } header { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid var(--pi-border); } .header-actions { display: flex; align-items: center; gap: 8px; } @@ -93,6 +95,9 @@ export const appStyles = css` .mobile-navigation-tab, .mobile-navigation-panel { display: none; } .mobile-tabs button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); } .tab-badge { display: inline-block; min-width: 14px; margin-left: 4px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; } + .expand-panel-strip { flex: 0 0 auto; display: flex; align-items: center; padding: 4px 8px; border-bottom: 1px solid var(--pi-border-muted); } + .expand-workspace-panel-button { display: inline-flex; align-items: center; justify-content: center; width: 30px; height: 30px; padding: 0; border-radius: 6px; } + .expand-workspace-panel-button .expand-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; } workspace-panel { min-width: 0; min-height: 0; border-left: 1px solid var(--pi-border); overflow: hidden; } @media (max-width: 1180px) { .shell { grid-template-columns: 340px minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); } @@ -102,6 +107,8 @@ export const appStyles = css` .shell.workspace-view main { grid-row: 1; min-height: auto; } .shell.workspace-view > workspace-panel { grid-column: 2; grid-row: 2; display: flex; border-left: 0; } .shell:not(.workspace-view) > workspace-panel { display: none; } + .shell.workspace-panel-collapsed > workspace-panel { display: none; } + .shell.workspace-panel-collapsed .expand-panel-strip { display: flex; } main.workspace-view chat-view, main.workspace-view prompt-editor, main.workspace-view status-bar, main.workspace-view .empty { display: none; } main.workspace-view { overflow: hidden; } @@ -140,8 +147,10 @@ export const workspacePanelStyles = css` .workspace-header-scroll-frame::after { right: 0; background: linear-gradient(270deg, color-mix(in srgb, var(--pi-shadow-strong) 55%, transparent) 0%, transparent 100%); } .workspace-header-scroll-frame.can-scroll-left::before, .workspace-header-scroll-frame.can-scroll-right::after { opacity: 1; } .workspace-header-strip { display: flex; justify-content: space-between; align-items: center; gap: 8px; min-width: 0; padding: 8px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; } - .tabs { flex: 0 0 auto; display: flex; gap: 6px; } + .tabs { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; } .tabs button { flex: 0 0 auto; white-space: nowrap; } + .collapse-button { display: inline-flex; align-items: center; justify-content: center; width: auto; height: auto; padding: 5px 7px; } + .collapse-icon { width: 14px; height: 14px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; } button { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 7px; cursor: pointer; } button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); } .tab-badge { display: inline-block; min-width: 14px; border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }