fix: simplify mobile breadcrumb machine context

This commit is contained in:
Federico Jaramillo Martinez
2026-06-05 20:43:45 +02:00
parent 66c5d0faca
commit ad963a2391
4 changed files with 44 additions and 27 deletions
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Simplify the mobile location breadcrumb by hiding the machine crumb when there is only one configured machine and removing activity indicators from breadcrumb items.
+1 -10
View File
@@ -34,7 +34,6 @@ import { readSettingsSection, writeSettingsSection, type SettingsSection } from
import { applyShortcutPreferences } from "../shortcutPreferences";
import { createTerminalCommandRunsRuntime } from "../runtime/terminalRuntime";
import { isWorkspaceDeletionPending, isWorkspaceDeletionRunPending, latestWorkspaceDeletionRuns, pendingWorkspaceDeletionIds, targetWorkspaceIdForRun, workspaceDeletionRunFilter } from "../workspaceDeletion";
import { machineActivityIndicator } from "../workspaceActivity";
import "./MachineList";
import "./ProjectList";
import "./WorkspaceList";
@@ -1326,8 +1325,8 @@ export class PiWebApp extends LitElement {
if (!this.appShell.isMobileNavigationLayout) return null;
return html`
<app-context-bar
.machines=${this.state.machines}
.machine=${this.state.selectedMachine}
.machineActivityKind=${selectedMachineActivityIndicator(this.state)}
.project=${this.state.selectedProject}
.workspace=${this.state.selectedWorkspace}
.session=${this.state.selectedSession}
@@ -1432,14 +1431,6 @@ function shouldRefreshMachineActivity(machine: Machine, health: MachineHealth |
return status === undefined || status === "unknown" || status === "online";
}
function selectedMachineActivityIndicator(state: AppState) {
const machineId = selectedMachineId(state);
const machine = state.selectedMachine;
const status = state.machineStatuses[machineId]?.status ?? machine?.status;
if (status === "offline" || status === "error") return undefined;
return machineActivityIndicator(state.machineActivities[machineId]);
}
function patchChangesState(state: AppState, patch: Partial<AppState>): boolean {
return Object.entries(patch).some(([key, value]) => Reflect.get(state, key) !== value);
}
@@ -0,0 +1,24 @@
import { describe, expect, it } from "vitest";
import type { Machine } from "../../api";
import { shouldShowMachineContext } from "./AppContextBar";
describe("shouldShowMachineContext", () => {
it("hides the machine crumb when there is no machine choice", () => {
expect(shouldShowMachineContext([])).toBe(false);
expect(shouldShowMachineContext([machine("local")])).toBe(false);
});
it("shows the machine crumb when multiple machines exist", () => {
expect(shouldShowMachineContext([machine("local"), machine("remote-a")])).toBe(true);
});
});
function machine(id: string): Machine {
return {
id,
name: id,
kind: id === "local" ? "local" : "remote",
createdAt: "2026-06-04T00:00:00.000Z",
updatedAt: "2026-06-04T00:00:00.000Z",
};
}
@@ -2,12 +2,11 @@ import { LitElement, css, html } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import type { Machine, Project, SessionInfo, Workspace } from "../../api";
import type { NavigationSection } from "../../appShell/navigationState";
import { renderActivityIndicator, type ActivityIndicatorKind } from "../activityBadge";
@customElement("app-context-bar")
export class AppContextBar extends LitElement {
@property({ attribute: false }) machines: Machine[] = [];
@property({ attribute: false }) machine?: Machine;
@property({ attribute: false }) machineActivityKind?: ActivityIndicatorKind;
@property({ attribute: false }) project?: Project;
@property({ attribute: false }) workspace?: Workspace;
@property({ attribute: false }) session?: SessionInfo;
@@ -38,6 +37,7 @@ export class AppContextBar extends LitElement {
}
override render() {
const showMachineContext = shouldShowMachineContext(this.machines);
const machineLabel = machineContextLabel(this.machine);
const projectLabel = projectContextLabel(this.project);
const workspaceLabel = workspaceContextLabel(this.workspace);
@@ -46,13 +46,14 @@ export class AppContextBar extends LitElement {
<nav class=${this.contextBarClass()} aria-label="Current location">
<span class="context-bar-label">Location</span>
<ol class="context-items" @scroll=${this.onContextScroll}>
<li class="context-item">
<button type="button" class=${this.machine === undefined ? "context-chip empty" : "context-chip"} title=${machineContextTitle(this.machine)} aria-label=${`Machine: ${machineLabel}. Open machine selection.`} @click=${() => { this.onOpenSection?.("machines"); }}>
<span class="context-kind">Machine</span>
${this.renderMachineActivity()}
<span class="context-value">${machineLabel}</span>
</button>
</li>
${showMachineContext ? html`
<li class="context-item">
<button type="button" class=${this.machine === undefined ? "context-chip empty" : "context-chip"} title=${machineContextTitle(this.machine)} aria-label=${`Machine: ${machineLabel}. Open machine selection.`} @click=${() => { this.onOpenSection?.("machines"); }}>
<span class="context-kind">Machine</span>
<span class="context-value">${machineLabel}</span>
</button>
</li>
` : null}
<li class="context-item">
<button type="button" class=${this.project === undefined ? "context-chip empty" : "context-chip"} title=${projectContextTitle(this.project)} aria-label=${`Project: ${projectLabel}. Open project selection.`} @click=${() => { this.onOpenSection?.("projects"); }}>
<span class="context-kind">Project</span>
@@ -77,10 +78,6 @@ export class AppContextBar extends LitElement {
`;
}
private renderMachineActivity() {
return renderActivityIndicator(this.machineActivityKind, this.machineActivityKind === "terminal" ? "Machine terminal active" : "Machine active");
}
private renderActionsButton() {
if (this.onShowActions === undefined) return null;
return html`
@@ -159,16 +156,16 @@ export class AppContextBar extends LitElement {
.context-chip:hover { background: var(--pi-surface-hover); }
.context-chip:focus-visible { outline: 2px solid var(--pi-accent); outline-offset: 2px; }
.context-chip.empty { border-style: dashed; color: var(--pi-muted); }
.activity-indicator { flex: 0 0 auto; display: inline-block; width: 7px; height: 7px; margin-right: 0; background: var(--pi-success); animation: pulse 1s ease-in-out infinite; vertical-align: 1px; }
.activity-indicator.session { border-radius: 50%; background: var(--pi-success); }
.activity-indicator.terminal { border-radius: 2px; background: var(--pi-accent); }
.context-kind { display: none; }
.context-value { min-width: 0; overflow: visible; text-overflow: clip; white-space: nowrap; }
button { cursor: pointer; }
@keyframes pulse { 0%, 100% { transform: scale(.75); opacity: .55; } 50% { transform: scale(1.2); opacity: 1; } }
`;
}
export function shouldShowMachineContext(machines: readonly Machine[]): boolean {
return machines.length > 1;
}
function machineContextLabel(machine: Machine | undefined): string {
return machine === undefined ? "No machine" : `${machine.name}${machine.kind === "remote" ? " · remote" : ""}`;
}