Archived
fix: improve workspace empty states
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@jmfederico/pi-web": patch
|
||||
---
|
||||
|
||||
Improve empty states for workspace tools and session selection when no project, workspace, or session is selected.
|
||||
@@ -11,6 +11,8 @@ export interface AppState {
|
||||
messagePageTotal: number;
|
||||
isLoadingEarlierMessages: boolean;
|
||||
isReceivingPartialStream: boolean;
|
||||
isLoadingProjects: boolean;
|
||||
isLoadingWorkspaces: boolean;
|
||||
selectedProject: Project | undefined;
|
||||
selectedWorkspace: Workspace | undefined;
|
||||
selectedSession: SessionInfo | undefined;
|
||||
@@ -96,6 +98,8 @@ export function initialAppState(): AppState {
|
||||
messagePageTotal: 0,
|
||||
isLoadingEarlierMessages: false,
|
||||
isReceivingPartialStream: false,
|
||||
isLoadingProjects: false,
|
||||
isLoadingWorkspaces: false,
|
||||
selectedProject: undefined,
|
||||
selectedWorkspace: undefined,
|
||||
selectedSession: undefined,
|
||||
|
||||
@@ -35,6 +35,7 @@ import "./ActionPalette";
|
||||
import "./AuthDialog";
|
||||
import "./ProjectDialog";
|
||||
import "./WorkspacePanel";
|
||||
import type { WorkspacePanelEmptyState } from "./WorkspacePanel";
|
||||
import { appStyles } from "./shared";
|
||||
|
||||
type NavigationSection = "projects" | "workspaces" | "sessions";
|
||||
@@ -418,7 +419,8 @@ export class PiWebApp extends LitElement {
|
||||
const workspace = this.state.selectedWorkspace;
|
||||
const panelContext = workspace === undefined ? undefined : this.createWorkspacePanelContext(workspace);
|
||||
const workspaceLabelItems = workspace === undefined ? [] : this.plugins.getWorkspaceLabelItems(this.state, workspace);
|
||||
return html`<workspace-panel .workspace=${workspace} .panelContext=${panelContext} .tool=${this.state.workspaceTool} .panels=${this.visibleWorkspacePanels()} .workspaceLabelItems=${workspaceLabelItems} .onSelectTool=${(tool: QualifiedContributionId) => { this.openWorkspaceTool(tool); }}></workspace-panel>`;
|
||||
const emptyState = workspace === undefined ? this.workspacePanelEmptyState() : undefined;
|
||||
return html`<workspace-panel .workspace=${workspace} .panelContext=${panelContext} .emptyState=${emptyState} .tool=${this.state.workspaceTool} .panels=${this.visibleWorkspacePanels()} .workspaceLabelItems=${workspaceLabelItems} .onSelectTool=${(tool: QualifiedContributionId) => { this.openWorkspaceTool(tool); }}></workspace-panel>`;
|
||||
}
|
||||
|
||||
private renderNavigationPanel(autoSwitchToChat: boolean) {
|
||||
@@ -509,6 +511,51 @@ export class PiWebApp extends LitElement {
|
||||
return this.plugins.getWorkspacePanels().filter((panel) => panel.visible?.({ workspace, state: this.state }) ?? true);
|
||||
}
|
||||
|
||||
private workspacePanelEmptyState(): WorkspacePanelEmptyState {
|
||||
const project = this.state.selectedProject;
|
||||
if (this.state.isLoadingProjects) {
|
||||
return {
|
||||
title: "Loading projects…",
|
||||
body: "Looking for projects you have added to Pi Web.",
|
||||
};
|
||||
}
|
||||
if (project === undefined) {
|
||||
return this.state.projects.length === 0
|
||||
? {
|
||||
title: "No projects yet",
|
||||
body: "Use Actions → Add Project to add a folder. Workspace tools will appear here after you choose a workspace.",
|
||||
}
|
||||
: {
|
||||
title: "Select a project",
|
||||
body: "Choose a project from the sidebar, then select a workspace to inspect files, Git, or terminals.",
|
||||
};
|
||||
}
|
||||
if (this.state.isLoadingWorkspaces) {
|
||||
return {
|
||||
title: "Loading workspaces…",
|
||||
body: `Preparing workspace tools for ${project.name}.`,
|
||||
};
|
||||
}
|
||||
if (this.state.workspaces.length === 0) {
|
||||
return {
|
||||
title: "No workspaces found",
|
||||
body: `${project.name} does not have any available workspaces. Try selecting the project again or re-adding it.`,
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: "Select a workspace",
|
||||
body: `Choose a workspace in ${project.name} to inspect files, Git, or terminals.`,
|
||||
};
|
||||
}
|
||||
|
||||
private sessionEmptyMessage(): string {
|
||||
if (this.state.isLoadingProjects) return "Loading projects…";
|
||||
if (this.state.selectedWorkspace !== undefined) return "Select or start a session.";
|
||||
if (this.state.selectedProject !== undefined) return "Select a workspace to start a session.";
|
||||
if (this.state.projects.length === 0) return "Add a project to start a session.";
|
||||
return "Select a project and workspace to start a session.";
|
||||
}
|
||||
|
||||
private renderMobilePanelTitle(panel: QualifiedWorkspacePanelContribution) {
|
||||
const workspace = this.state.selectedWorkspace;
|
||||
if (workspace === undefined) return panel.title;
|
||||
@@ -847,7 +894,7 @@ export class PiWebApp extends LitElement {
|
||||
${state.modelDialog !== undefined ? html`<command-picker title=${state.modelDialog.title} .searchable=${true} .options=${state.modelDialog.options} .selectedValue=${state.modelDialog.selectedValue} .onPick=${(value: string) => { void this.pickModel(value); }} .onCancel=${() => { this.setState({ modelDialog: undefined }); }}></command-picker>` : null}
|
||||
${state.thinkingDialog !== undefined ? html`<command-picker title=${state.thinkingDialog.title} .options=${state.thinkingDialog.options} .selectedValue=${state.thinkingDialog.selectedValue} .onPick=${(value: string) => { void this.pickThinking(value); }} .onCancel=${() => { this.setState({ thinkingDialog: undefined }); }}></command-picker>` : null}
|
||||
${state.authDialog !== undefined ? html`<auth-dialog .state=${state.authDialog} .onChooseMethod=${(authType: "oauth" | "api_key") => { void this.auth.chooseLoginMethod(authType); }} .onSelectProvider=${(providerId: string, authType: "oauth" | "api_key") => { void this.auth.selectLoginProvider(providerId, authType); }} .onApiKeyInput=${(value: string) => { this.auth.updateApiKey(value); }} .onSaveApiKey=${() => { void this.auth.saveApiKey(); }} .onLogoutProvider=${(providerId: string) => { void this.auth.logoutProvider(providerId); }} .onOAuthInput=${(value: string) => { this.auth.updateOAuthInput(value); }} .onOAuthRespond=${(value?: string) => { void this.auth.respondOAuth(value); }} .onOAuthCancel=${() => { void this.auth.cancelOAuth(); }} .onCancel=${() => { this.auth.closeDialog(); }}></auth-dialog>` : null}
|
||||
` : html`<div class="empty">Select or start a session.</div>`}
|
||||
` : html`<div class="empty">${this.sessionEmptyMessage()}</div>`}
|
||||
</main>
|
||||
${this.renderWorkspacePanel()}
|
||||
${state.actionPaletteOpen ? html`<action-palette .actions=${this.getActions()} .onRun=${(action: AppAction) => { this.setState({ actionPaletteOpen: false }); this.runAction(action); }} .onCancel=${() => { this.setState({ actionPaletteOpen: false }); }}></action-palette>` : null}
|
||||
|
||||
@@ -5,10 +5,16 @@ import type { QualifiedContributionId, QualifiedWorkspacePanelContribution, Work
|
||||
import { workspacePanelStyles } from "./shared";
|
||||
import { renderWorkspaceLabel } from "./workspaceLabel";
|
||||
|
||||
export interface WorkspacePanelEmptyState {
|
||||
title: string;
|
||||
body?: string;
|
||||
}
|
||||
|
||||
@customElement("workspace-panel")
|
||||
export class WorkspacePanel extends LitElement {
|
||||
@property({ attribute: false }) workspace: Workspace | undefined;
|
||||
@property({ attribute: false }) panelContext: WorkspacePanelContext | undefined;
|
||||
@property({ attribute: false }) emptyState: WorkspacePanelEmptyState | undefined;
|
||||
@property() tool: QualifiedContributionId = "core:workspace.files";
|
||||
@property({ attribute: false }) panels: QualifiedWorkspacePanelContribution[] = [];
|
||||
@property({ attribute: false }) workspaceLabelItems: WorkspaceLabelItem[] = [];
|
||||
@@ -43,9 +49,15 @@ export class WorkspacePanel extends LitElement {
|
||||
|
||||
override render() {
|
||||
const workspace = this.workspace;
|
||||
if (workspace === undefined) return html`<section class="empty">Select a workspace.</section>`;
|
||||
if (workspace === undefined) return this.renderEmptyState(this.emptyState ?? {
|
||||
title: "Select a workspace",
|
||||
body: "Choose a workspace to inspect files, Git, or terminals.",
|
||||
});
|
||||
const context = this.panelContext;
|
||||
if (context === undefined) return html`<section class="empty">Workspace panel unavailable.</section>`;
|
||||
if (context === undefined) return this.renderEmptyState({
|
||||
title: "Workspace tools unavailable",
|
||||
body: "Try selecting the workspace again.",
|
||||
});
|
||||
const visiblePanels = this.panels;
|
||||
const selectedPanel = visiblePanels.find((panel) => panel.id === this.tool) ?? visiblePanels[0];
|
||||
return html`
|
||||
@@ -63,7 +75,10 @@ export class WorkspacePanel extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
${selectedPanel === undefined ? html`<section class="empty">No workspace panels registered.</section>` : html`
|
||||
${selectedPanel === undefined ? this.renderEmptyState({
|
||||
title: "No workspace tools available",
|
||||
body: "No tools are available for this workspace.",
|
||||
}) : html`
|
||||
<div class="panel-content">
|
||||
${selectedPanel.render(context)}
|
||||
</div>
|
||||
@@ -77,6 +92,15 @@ export class WorkspacePanel extends LitElement {
|
||||
return html`${panel.title} <span class="tab-badge">${badge}</span>`;
|
||||
}
|
||||
|
||||
private renderEmptyState(state: WorkspacePanelEmptyState): TemplateResult {
|
||||
return html`
|
||||
<section class="empty-state" role="status">
|
||||
<h2>${state.title}</h2>
|
||||
${state.body === undefined ? null : html`<p>${state.body}</p>`}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
private workspaceHeaderFrameClass(): string {
|
||||
return `workspace-header-scroll-frame${this.workspaceHeaderCanScrollLeft ? " can-scroll-left" : ""}${this.workspaceHeaderCanScrollRight ? " can-scroll-right" : ""}`;
|
||||
}
|
||||
|
||||
@@ -130,6 +130,9 @@ export const workspacePanelStyles = css`
|
||||
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; }
|
||||
.panel-content { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: auto; }
|
||||
.empty-state { box-sizing: border-box; width: min(100%, 380px); margin: auto; padding: 24px; display: grid; gap: 8px; color: var(--pi-muted); text-align: center; }
|
||||
.empty-state h2 { margin: 0; color: var(--pi-text); font-size: 15px; line-height: 1.3; }
|
||||
.empty-state p { margin: 0; line-height: 1.45; }
|
||||
small, .muted { color: var(--pi-muted); }
|
||||
header small { flex: 0 0 auto; min-width: max-content; overflow: visible; text-overflow: clip; white-space: nowrap; }
|
||||
header .workspace-label { width: max-content; max-width: none; overflow: visible; }
|
||||
|
||||
@@ -6,7 +6,7 @@ export class ProjectController {
|
||||
constructor(private readonly getState: GetState, private readonly setState: SetState, private readonly workspaces: WorkspaceController) {}
|
||||
|
||||
async loadProjects() {
|
||||
this.setState({ error: "" });
|
||||
this.setState({ error: "", isLoadingProjects: true });
|
||||
try {
|
||||
const projects = await api.projects();
|
||||
const projectIds = new Set(projects.map((project) => project.id));
|
||||
@@ -14,6 +14,8 @@ export class ProjectController {
|
||||
this.setState({ projects, workspacesByProjectId });
|
||||
} catch (error) {
|
||||
this.setState({ error: String(error) });
|
||||
} finally {
|
||||
this.setState({ isLoadingProjects: false });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export class WorkspaceController {
|
||||
|
||||
clearSelection(options?: { updateUrl?: boolean | undefined }) {
|
||||
this.sessions.clearActiveSession();
|
||||
this.setState({ selectedProject: undefined, selectedWorkspace: undefined, workspaces: [], ...resetWorkspaceScopedState() });
|
||||
this.setState({ selectedProject: undefined, selectedWorkspace: undefined, workspaces: [], isLoadingWorkspaces: false, ...resetWorkspaceScopedState() });
|
||||
if (options?.updateUrl !== false) this.updateUrl();
|
||||
}
|
||||
|
||||
@@ -28,22 +28,22 @@ export class WorkspaceController {
|
||||
|
||||
async selectProject(project: Project, target?: RouteTarget) {
|
||||
this.sessions.clearActiveSession();
|
||||
this.setState({ selectedProject: project, selectedWorkspace: undefined, workspaces: [], ...resetWorkspaceScopedState() });
|
||||
this.setState({ selectedProject: project, selectedWorkspace: undefined, workspaces: [], isLoadingWorkspaces: true, ...resetWorkspaceScopedState() });
|
||||
try {
|
||||
const workspaces = await api.workspaces(project.id);
|
||||
this.setState({ workspaces, workspacesByProjectId: { ...this.getState().workspacesByProjectId, [project.id]: workspaces } });
|
||||
this.setState({ workspaces, workspacesByProjectId: { ...this.getState().workspacesByProjectId, [project.id]: workspaces }, isLoadingWorkspaces: false });
|
||||
const workspace = selectPreferredWorkspace(workspaces, { targetWorkspaceId: target?.workspaceId, latestWorkspaceId: this.workspaceSelection.latestWorkspaceId(project.id) });
|
||||
if (workspace) await this.selectWorkspace(workspace, { sessionId: target?.sessionId, updateUrl: target?.updateUrl });
|
||||
else if (target?.updateUrl !== false) this.updateUrl();
|
||||
} catch (error) {
|
||||
this.setState({ error: String(error) });
|
||||
this.setState({ error: String(error), isLoadingWorkspaces: false });
|
||||
}
|
||||
}
|
||||
|
||||
async selectWorkspace(workspace: Workspace, target?: { sessionId?: string | undefined; updateUrl?: boolean | undefined }) {
|
||||
this.workspaceSelection.rememberWorkspace(workspace);
|
||||
this.sessions.clearActiveSession();
|
||||
this.setState({ selectedWorkspace: workspace, ...resetWorkspaceScopedState() });
|
||||
this.setState({ selectedWorkspace: workspace, isLoadingWorkspaces: false, ...resetWorkspaceScopedState() });
|
||||
try {
|
||||
const sessions = mergeCachedNewSessions(workspace.path, await api.sessions(workspace.path));
|
||||
this.setState({ sessions });
|
||||
|
||||
Reference in New Issue
Block a user