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 { applyShortcutPreferences } from "../shortcutPreferences";
import { createTerminalCommandRunsRuntime } from "../runtime/terminalRuntime"; import { createTerminalCommandRunsRuntime } from "../runtime/terminalRuntime";
import { isWorkspaceDeletionPending, isWorkspaceDeletionRunPending, latestWorkspaceDeletionRuns, pendingWorkspaceDeletionIds, targetWorkspaceIdForRun, workspaceDeletionRunFilter } from "../workspaceDeletion"; import { isWorkspaceDeletionPending, isWorkspaceDeletionRunPending, latestWorkspaceDeletionRuns, pendingWorkspaceDeletionIds, targetWorkspaceIdForRun, workspaceDeletionRunFilter } from "../workspaceDeletion";
import { machineActivityIndicator } from "../workspaceActivity";
import "./MachineList"; import "./MachineList";
import "./ProjectList"; import "./ProjectList";
import "./WorkspaceList"; import "./WorkspaceList";
@@ -1326,8 +1325,8 @@ export class PiWebApp extends LitElement {
if (!this.appShell.isMobileNavigationLayout) return null; if (!this.appShell.isMobileNavigationLayout) return null;
return html` return html`
<app-context-bar <app-context-bar
.machines=${this.state.machines}
.machine=${this.state.selectedMachine} .machine=${this.state.selectedMachine}
.machineActivityKind=${selectedMachineActivityIndicator(this.state)}
.project=${this.state.selectedProject} .project=${this.state.selectedProject}
.workspace=${this.state.selectedWorkspace} .workspace=${this.state.selectedWorkspace}
.session=${this.state.selectedSession} .session=${this.state.selectedSession}
@@ -1432,14 +1431,6 @@ function shouldRefreshMachineActivity(machine: Machine, health: MachineHealth |
return status === undefined || status === "unknown" || status === "online"; 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 { function patchChangesState(state: AppState, patch: Partial<AppState>): boolean {
return Object.entries(patch).some(([key, value]) => Reflect.get(state, key) !== value); 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 { customElement, property, query, state } from "lit/decorators.js";
import type { Machine, Project, SessionInfo, Workspace } from "../../api"; import type { Machine, Project, SessionInfo, Workspace } from "../../api";
import type { NavigationSection } from "../../appShell/navigationState"; import type { NavigationSection } from "../../appShell/navigationState";
import { renderActivityIndicator, type ActivityIndicatorKind } from "../activityBadge";
@customElement("app-context-bar") @customElement("app-context-bar")
export class AppContextBar extends LitElement { export class AppContextBar extends LitElement {
@property({ attribute: false }) machines: Machine[] = [];
@property({ attribute: false }) machine?: Machine; @property({ attribute: false }) machine?: Machine;
@property({ attribute: false }) machineActivityKind?: ActivityIndicatorKind;
@property({ attribute: false }) project?: Project; @property({ attribute: false }) project?: Project;
@property({ attribute: false }) workspace?: Workspace; @property({ attribute: false }) workspace?: Workspace;
@property({ attribute: false }) session?: SessionInfo; @property({ attribute: false }) session?: SessionInfo;
@@ -38,6 +37,7 @@ export class AppContextBar extends LitElement {
} }
override render() { override render() {
const showMachineContext = shouldShowMachineContext(this.machines);
const machineLabel = machineContextLabel(this.machine); const machineLabel = machineContextLabel(this.machine);
const projectLabel = projectContextLabel(this.project); const projectLabel = projectContextLabel(this.project);
const workspaceLabel = workspaceContextLabel(this.workspace); const workspaceLabel = workspaceContextLabel(this.workspace);
@@ -46,13 +46,14 @@ export class AppContextBar extends LitElement {
<nav class=${this.contextBarClass()} aria-label="Current location"> <nav class=${this.contextBarClass()} aria-label="Current location">
<span class="context-bar-label">Location</span> <span class="context-bar-label">Location</span>
<ol class="context-items" @scroll=${this.onContextScroll}> <ol class="context-items" @scroll=${this.onContextScroll}>
${showMachineContext ? html`
<li class="context-item"> <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"); }}> <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-kind">Machine</span>
${this.renderMachineActivity()}
<span class="context-value">${machineLabel}</span> <span class="context-value">${machineLabel}</span>
</button> </button>
</li> </li>
` : null}
<li class="context-item"> <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"); }}> <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> <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() { private renderActionsButton() {
if (this.onShowActions === undefined) return null; if (this.onShowActions === undefined) return null;
return html` return html`
@@ -159,16 +156,16 @@ export class AppContextBar extends LitElement {
.context-chip:hover { background: var(--pi-surface-hover); } .context-chip:hover { background: var(--pi-surface-hover); }
.context-chip:focus-visible { outline: 2px solid var(--pi-accent); outline-offset: 2px; } .context-chip:focus-visible { outline: 2px solid var(--pi-accent); outline-offset: 2px; }
.context-chip.empty { border-style: dashed; color: var(--pi-muted); } .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-kind { display: none; }
.context-value { min-width: 0; overflow: visible; text-overflow: clip; white-space: nowrap; } .context-value { min-width: 0; overflow: visible; text-overflow: clip; white-space: nowrap; }
button { cursor: pointer; } 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 { function machineContextLabel(machine: Machine | undefined): string {
return machine === undefined ? "No machine" : `${machine.name}${machine.kind === "remote" ? " · remote" : ""}`; return machine === undefined ? "No machine" : `${machine.name}${machine.kind === "remote" ? " · remote" : ""}`;
} }