diff --git a/src/client/src/components/WorkspacePanel.ts b/src/client/src/components/WorkspacePanel.ts
index 43f30292..454196dd 100644
--- a/src/client/src/components/WorkspacePanel.ts
+++ b/src/client/src/components/WorkspacePanel.ts
@@ -11,6 +11,9 @@ export interface WorkspacePanelEmptyState {
type WorkspacePanelBadge = string | number | TemplateResult | undefined;
+const visiblePanelTabLimit = 5;
+const pinnedWorkspaceTabsStorageKey = "pi-web.workspacePanel.pinnedTabs";
+
@customElement("workspace-panel")
export class WorkspacePanel extends LitElement {
@property({ attribute: false }) workspace: Workspace | undefined;
@@ -23,6 +26,7 @@ export class WorkspacePanel extends LitElement {
@query(".workspace-header-strip") private workspaceHeaderStrip?: HTMLElement | null;
@state() private workspaceHeaderCanScrollLeft = false;
@state() private workspaceHeaderCanScrollRight = false;
+ @state() private pinnedPanelIds: readonly QualifiedContributionId[] = loadPinnedPanelIds();
private observedWorkspaceHeaderStrip: HTMLElement | undefined;
private workspaceHeaderResizeObserver: ResizeObserver | undefined;
@@ -60,23 +64,30 @@ export class WorkspacePanel extends LitElement {
});
const visiblePanels = this.panels;
const selectedPanel = visiblePanels.find((panel) => panel.id === this.tool) ?? visiblePanels[0];
+ const visibleTabPanels = this.visibleTabPanels(visiblePanels);
+ const visibleTabPanelIds = new Set(visibleTabPanels.map((panel) => panel.id));
+ const overflowPanels = visiblePanels.filter((panel) => !visibleTabPanelIds.has(panel.id));
+ const selectedOverflowPanel = overflowPanels.some((panel) => panel.id === selectedPanel?.id) ? selectedPanel : undefined;
return html`
${this.hideToolTabs ? null : html`
@@ -92,6 +103,49 @@ export class WorkspacePanel extends LitElement {
`;
}
+ private onOverflowToolSelectChange = (event: Event): void => {
+ const target = event.currentTarget;
+ if (!(target instanceof HTMLSelectElement)) return;
+ const tool = qualifiedContributionId(target.value);
+ if (tool === undefined) return;
+ this.pinPanel(tool);
+ this.onSelectTool(tool);
+ };
+
+ private renderPanelTab(panel: QualifiedWorkspacePanelContribution, context: WorkspacePanelContext, selected: boolean): TemplateResult {
+ const badge = panel.badge?.(context);
+ const ariaLabel = this.panelTabAriaLabel(panel, badge);
+ return html`
+
+ `;
+ }
+
+ private visibleTabPanels(panels: QualifiedWorkspacePanelContribution[]): QualifiedWorkspacePanelContribution[] {
+ const byId = new Map(panels.map((panel) => [panel.id, panel]));
+ const chosen: QualifiedWorkspacePanelContribution[] = [];
+ const seen = new Set();
+ const add = (panel: QualifiedWorkspacePanelContribution | undefined): void => {
+ if (panel === undefined || seen.has(panel.id) || chosen.length >= visiblePanelTabLimit) return;
+ chosen.push(panel);
+ seen.add(panel.id);
+ };
+ for (const id of this.pinnedPanelIds) add(byId.get(id));
+ for (const panel of panels) add(panel);
+ return chosen;
+ }
+
+ private pinPanel(panelId: QualifiedContributionId): void {
+ const currentPinned = this.visibleTabPanels(this.panels).map((panel) => panel.id);
+ if (currentPinned.includes(panelId)) return;
+ const nextPinned = currentPinned.length < visiblePanelTabLimit
+ ? [...currentPinned, panelId]
+ : [...currentPinned.slice(0, visiblePanelTabLimit - 1), panelId];
+ this.pinnedPanelIds = nextPinned;
+ savePinnedPanelIds(nextPinned);
+ }
+
private panelTabClass(panel: QualifiedWorkspacePanelContribution, selected: boolean): string {
return [
...(panel.icon === undefined ? [] : ["icon-tab"]),
@@ -113,6 +167,12 @@ export class WorkspacePanel extends LitElement {
`;
}
+ private panelOptionLabel(panel: QualifiedWorkspacePanelContribution, badge: WorkspacePanelBadge): string {
+ if (typeof badge !== "string" && typeof badge !== "number") return panel.title;
+ const trimmedBadge = String(badge).trim();
+ return trimmedBadge === "" ? panel.title : `${panel.title} (${trimmedBadge})`;
+ }
+
private isEmptyBadge(badge: WorkspacePanelBadge): boolean {
return badge === undefined || badge === "";
}
@@ -159,3 +219,33 @@ export class WorkspacePanel extends LitElement {
static override styles = workspacePanelStyles;
}
+
+function qualifiedContributionId(value: string): QualifiedContributionId | undefined {
+ const separator = value.indexOf(":");
+ if (separator <= 0 || separator >= value.length - 1) return undefined;
+ const pluginId = value.slice(0, separator);
+ const localId = value.slice(separator + 1);
+ return `${pluginId}:${localId}`;
+}
+
+function loadPinnedPanelIds(): QualifiedContributionId[] {
+ if (typeof window === "undefined") return [];
+ try {
+ const raw = window.localStorage.getItem(pinnedWorkspaceTabsStorageKey);
+ if (raw === null) return [];
+ const value: unknown = JSON.parse(raw);
+ if (!Array.isArray(value)) return [];
+ return value.flatMap((item) => typeof item === "string" ? qualifiedContributionId(item) ?? [] : []);
+ } catch {
+ return [];
+ }
+}
+
+function savePinnedPanelIds(panelIds: readonly QualifiedContributionId[]): void {
+ if (typeof window === "undefined") return;
+ try {
+ window.localStorage.setItem(pinnedWorkspaceTabsStorageKey, JSON.stringify(panelIds));
+ } catch {
+ // Ignore storage failures; tabs still update for this render cycle.
+ }
+}
diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts
index df2e812e..3df8d7ea 100644
--- a/src/client/src/components/shared.ts
+++ b/src/client/src/components/shared.ts
@@ -158,7 +158,12 @@ export const workspacePanelStyles = css`
.workspace-header-scroll-frame::before { left: 0; background: linear-gradient(90deg, color-mix(in srgb, var(--pi-shadow-strong) 55%, transparent) 0%, transparent 100%); }
.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; }
+ .workspace-header-strip { display: flex; align-items: center; gap: 8px; min-width: 0; padding: 8px; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: thin; }
+ .workspace-tabs { flex: 0 0 auto; }
+ .overflow-tab-select { flex: 0 0 auto; min-width: 0; }
+ .overflow-tab-select span { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }
+ .overflow-tab-select select { max-width: 180px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 26px 5px 8px; font: inherit; cursor: pointer; }
+ .overflow-tab-select select:focus { outline: 2px solid color-mix(in srgb, var(--pi-accent) 45%, transparent); outline-offset: 1px; }
.tabs { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; }
.tabs button { flex: 0 0 auto; white-space: nowrap; }
.tabs button.icon-tab { min-width: 34px; }