diff --git a/.changeset/friendly-workspace-empty-states.md b/.changeset/friendly-workspace-empty-states.md
new file mode 100644
index 0000000..f980d40
--- /dev/null
+++ b/.changeset/friendly-workspace-empty-states.md
@@ -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.
diff --git a/src/client/src/appState.ts b/src/client/src/appState.ts
index d891d11..9a25dd5 100644
--- a/src/client/src/appState.ts
+++ b/src/client/src/appState.ts
@@ -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,
diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts
index 3bdd0ee..d6a7938 100644
--- a/src/client/src/components/PiWebApp.ts
+++ b/src/client/src/components/PiWebApp.ts
@@ -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` { this.openWorkspaceTool(tool); }}>`;
+ const emptyState = workspace === undefined ? this.workspacePanelEmptyState() : undefined;
+ return html` { this.openWorkspaceTool(tool); }}>`;
}
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` { void this.pickModel(value); }} .onCancel=${() => { this.setState({ modelDialog: undefined }); }}>` : null}
${state.thinkingDialog !== undefined ? html` { void this.pickThinking(value); }} .onCancel=${() => { this.setState({ thinkingDialog: undefined }); }}>` : null}
${state.authDialog !== undefined ? html` { 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(); }}>` : null}
- ` : html`
Select or start a session.
`}
+ ` : html`${this.sessionEmptyMessage()}
`}
${this.renderWorkspacePanel()}
${state.actionPaletteOpen ? html` { this.setState({ actionPaletteOpen: false }); this.runAction(action); }} .onCancel=${() => { this.setState({ actionPaletteOpen: false }); }}>` : null}
diff --git a/src/client/src/components/WorkspacePanel.ts b/src/client/src/components/WorkspacePanel.ts
index 57eddeb..826b293 100644
--- a/src/client/src/components/WorkspacePanel.ts
+++ b/src/client/src/components/WorkspacePanel.ts
@@ -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``;
+ 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`Workspace panel unavailable.`;
+ 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 {
- ${selectedPanel === undefined ? html`No workspace panels registered.` : html`
+ ${selectedPanel === undefined ? this.renderEmptyState({
+ title: "No workspace tools available",
+ body: "No tools are available for this workspace.",
+ }) : html`
${selectedPanel.render(context)}
@@ -77,6 +92,15 @@ export class WorkspacePanel extends LitElement {
return html`${panel.title} ${badge}`;
}
+ private renderEmptyState(state: WorkspacePanelEmptyState): TemplateResult {
+ return html`
+
+ ${state.title}
+ ${state.body === undefined ? null : html`${state.body}
`}
+
+ `;
+ }
+
private workspaceHeaderFrameClass(): string {
return `workspace-header-scroll-frame${this.workspaceHeaderCanScrollLeft ? " can-scroll-left" : ""}${this.workspaceHeaderCanScrollRight ? " can-scroll-right" : ""}`;
}
diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts
index 3f79fb4..59578fb 100644
--- a/src/client/src/components/shared.ts
+++ b/src/client/src/components/shared.ts
@@ -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; }
diff --git a/src/client/src/controllers/projectController.ts b/src/client/src/controllers/projectController.ts
index 9ed98d5..21528a6 100644
--- a/src/client/src/controllers/projectController.ts
+++ b/src/client/src/controllers/projectController.ts
@@ -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 });
}
}
diff --git a/src/client/src/controllers/workspaceController.ts b/src/client/src/controllers/workspaceController.ts
index df33124..405c824 100644
--- a/src/client/src/controllers/workspaceController.ts
+++ b/src/client/src/controllers/workspaceController.ts
@@ -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 });