Archived
Improve mobile single-panel navigation
This commit is contained in:
@@ -21,7 +21,7 @@ export interface AppState {
|
|||||||
actionPaletteOpen: boolean;
|
actionPaletteOpen: boolean;
|
||||||
projectDialogOpen: boolean;
|
projectDialogOpen: boolean;
|
||||||
workspaceTool: QualifiedContributionId;
|
workspaceTool: QualifiedContributionId;
|
||||||
mainView: "chat" | QualifiedContributionId;
|
mainView: "navigation" | "chat" | QualifiedContributionId;
|
||||||
fileTree: FileTreeEntry[];
|
fileTree: FileTreeEntry[];
|
||||||
expandedDirs: Record<string, FileTreeEntry[]>;
|
expandedDirs: Record<string, FileTreeEntry[]>;
|
||||||
selectedFilePath: string | undefined;
|
selectedFilePath: string | undefined;
|
||||||
|
|||||||
@@ -138,7 +138,7 @@ export class PiWebApp extends LitElement {
|
|||||||
workspaceId: this.state.selectedWorkspace?.id,
|
workspaceId: this.state.selectedWorkspace?.id,
|
||||||
sessionId: this.state.selectedSession?.id,
|
sessionId: this.state.selectedSession?.id,
|
||||||
tool: this.state.workspaceTool,
|
tool: this.state.workspaceTool,
|
||||||
view: this.state.mainView,
|
view: this.state.mainView === "navigation" ? undefined : this.state.mainView,
|
||||||
file: this.state.selectedFilePath,
|
file: this.state.selectedFilePath,
|
||||||
diff: this.state.selectedDiffPath,
|
diff: this.state.selectedDiffPath,
|
||||||
});
|
});
|
||||||
@@ -152,6 +152,12 @@ export class PiWebApp extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private selectMainView(view: AppState["mainView"]) {
|
private selectMainView(view: AppState["mainView"]) {
|
||||||
|
if (view === "navigation") {
|
||||||
|
this.setState({ mainView: view });
|
||||||
|
this.updateUrl();
|
||||||
|
this.git.updatePolling();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.setState({ mainView: view, workspaceTool: view === "chat" ? this.state.workspaceTool : view });
|
this.setState({ mainView: view, workspaceTool: view === "chat" ? this.state.workspaceTool : view });
|
||||||
this.updateUrl();
|
this.updateUrl();
|
||||||
if (view !== "chat") this.refreshSelectedWorkspaceTool(view);
|
if (view !== "chat") this.refreshSelectedWorkspaceTool(view);
|
||||||
@@ -178,8 +184,25 @@ export class PiWebApp extends LitElement {
|
|||||||
if (tool === "core:workspace.git") void this.git.refreshGit();
|
if (tool === "core:workspace.git") void this.git.refreshGit();
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderWorkspacePanel() {
|
private renderWorkspacePanel(hideToolTabs = false) {
|
||||||
return html`<workspace-panel .workspace=${this.state.selectedWorkspace} .tool=${this.state.workspaceTool} .panels=${this.visibleWorkspacePanels()} .fileTree=${this.state.fileTree} .expandedDirs=${this.state.expandedDirs} .selectedFilePath=${this.state.selectedFilePath} .selectedFileContent=${this.state.selectedFileContent} .fileTreeStale=${this.state.fileTreeStale} .gitStatus=${this.state.gitStatus} .selectedDiffPath=${this.state.selectedDiffPath} .selectedDiff=${this.state.selectedDiff} .selectedStagedDiff=${this.state.selectedStagedDiff} .gitStale=${this.state.gitStale} .onSelectTool=${(tool: QualifiedContributionId) => { this.selectWorkspaceTool(tool); }} .onRefreshFiles=${() => this.files.refreshFiles()} .onExpandDir=${(path: string) => this.files.expandDir(path)} .onSelectFile=${(path: string) => this.files.selectFile(path)} .onRefreshGit=${() => this.git.refreshGit()} .onSelectDiff=${(path: string) => this.git.selectDiff(path)}></workspace-panel>`;
|
return html`<workspace-panel .workspace=${this.state.selectedWorkspace} .tool=${this.state.workspaceTool} .panels=${this.visibleWorkspacePanels()} .hideToolTabs=${hideToolTabs} .fileTree=${this.state.fileTree} .expandedDirs=${this.state.expandedDirs} .selectedFilePath=${this.state.selectedFilePath} .selectedFileContent=${this.state.selectedFileContent} .fileTreeStale=${this.state.fileTreeStale} .gitStatus=${this.state.gitStatus} .selectedDiffPath=${this.state.selectedDiffPath} .selectedDiff=${this.state.selectedDiff} .selectedStagedDiff=${this.state.selectedStagedDiff} .gitStale=${this.state.gitStale} .onSelectTool=${(tool: QualifiedContributionId) => { this.selectWorkspaceTool(tool); }} .onRefreshFiles=${() => this.files.refreshFiles()} .onExpandDir=${(path: string) => this.files.expandDir(path)} .onSelectFile=${(path: string) => this.files.selectFile(path)} .onRefreshGit=${() => this.git.refreshGit()} .onSelectDiff=${(path: string) => this.git.selectDiff(path)}></workspace-panel>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderNavigationPanel(autoSwitchToChat: boolean) {
|
||||||
|
const openChatAfter = (action: () => Promise<void>) => this.withChatScrollTransition(async () => {
|
||||||
|
await action();
|
||||||
|
if (autoSwitchToChat) this.setState({ mainView: "chat" });
|
||||||
|
if (autoSwitchToChat) this.updateUrl();
|
||||||
|
});
|
||||||
|
return html`
|
||||||
|
<header>
|
||||||
|
<strong>Pi Web POC</strong>
|
||||||
|
<button @click=${() => { this.setState({ projectDialogOpen: true }); }}>+ Project</button>
|
||||||
|
</header>
|
||||||
|
<project-list .projects=${this.state.projects} .selected=${this.state.selectedProject} .onSelect=${(project: Project) => this.withChatScrollTransition(() => this.workspaces.selectProject(project))} .onClose=${(project: Project) => this.projects.closeProject(project.id)}></project-list>
|
||||||
|
<workspace-list .workspaces=${this.state.workspaces} .selected=${this.state.selectedWorkspace} .onSelect=${(workspace: Workspace) => openChatAfter(() => this.workspaces.selectWorkspace(workspace))}></workspace-list>
|
||||||
|
<session-list .sessions=${this.state.sessions} .statuses=${this.state.sessionStatuses} .activities=${this.state.sessionActivities} .selected=${this.state.selectedSession} .canStart=${!!this.state.selectedWorkspace} .onStart=${() => openChatAfter(() => this.sessions.startSession())} .onSelect=${(session: SessionInfo) => openChatAfter(() => this.sessions.selectSession(session))} .onArchive=${(session: SessionInfo) => this.sessions.archiveSession(session)} .onRestore=${(session: SessionInfo) => openChatAfter(() => this.sessions.restoreSession(session))}></session-list>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private visibleWorkspacePanels(): QualifiedWorkspacePanelContribution[] {
|
private visibleWorkspacePanels(): QualifiedWorkspacePanelContribution[] {
|
||||||
@@ -216,30 +239,24 @@ export class PiWebApp extends LitElement {
|
|||||||
const state = this.state;
|
const state = this.state;
|
||||||
return html`
|
return html`
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<aside>
|
<aside>${this.renderNavigationPanel(false)}</aside>
|
||||||
<header>
|
<main class=${state.mainView === "chat" ? "chat-view" : state.mainView === "navigation" ? "navigation-view" : "workspace-view"}>
|
||||||
<strong>Pi Web POC</strong>
|
|
||||||
<button @click=${() => { this.setState({ projectDialogOpen: true }); }}>+ Project</button>
|
|
||||||
</header>
|
|
||||||
<project-list .projects=${state.projects} .selected=${state.selectedProject} .onSelect=${(project: Project) => this.withChatScrollTransition(() => this.workspaces.selectProject(project))} .onClose=${(project: Project) => this.projects.closeProject(project.id)}></project-list>
|
|
||||||
<workspace-list .workspaces=${state.workspaces} .selected=${state.selectedWorkspace} .onSelect=${(workspace: Workspace) => this.withChatScrollTransition(() => this.workspaces.selectWorkspace(workspace))}></workspace-list>
|
|
||||||
<session-list .sessions=${state.sessions} .statuses=${state.sessionStatuses} .activities=${state.sessionActivities} .selected=${state.selectedSession} .canStart=${!!state.selectedWorkspace} .onStart=${() => this.withChatScrollTransition(() => this.sessions.startSession())} .onSelect=${(session: SessionInfo) => this.withChatScrollTransition(() => this.sessions.selectSession(session))} .onArchive=${(session: SessionInfo) => this.sessions.archiveSession(session)} .onRestore=${(session: SessionInfo) => this.sessions.restoreSession(session)}></session-list>
|
|
||||||
</aside>
|
|
||||||
<main class=${state.mainView === "chat" ? "chat-view" : "workspace-view"}>
|
|
||||||
<div class="mobile-tabs">
|
<div class="mobile-tabs">
|
||||||
|
<button class=${state.mainView === "navigation" ? "mobile-navigation-tab selected" : "mobile-navigation-tab"} @click=${() => { this.selectMainView("navigation"); }}>Sessions</button>
|
||||||
<button class=${state.mainView === "chat" ? "selected" : ""} @click=${() => { this.selectMainView("chat"); }}>Chat</button>
|
<button class=${state.mainView === "chat" ? "selected" : ""} @click=${() => { this.selectMainView("chat"); }}>Chat</button>
|
||||||
${this.visibleWorkspacePanels().map((panel) => html`
|
${this.visibleWorkspacePanels().map((panel) => html`
|
||||||
<button class=${state.mainView === panel.id ? "selected" : ""} @click=${() => { this.selectMainView(panel.id); }}>${panel.title}</button>
|
<button class=${state.mainView === panel.id ? "selected" : ""} @click=${() => { this.selectMainView(panel.id); }}>${panel.title}</button>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
${state.error ? html`<div class="error">${state.error}</div>` : null}
|
${state.error ? html`<div class="error">${state.error}</div>` : null}
|
||||||
|
<div class="mobile-navigation-panel">${this.renderNavigationPanel(true)}</div>
|
||||||
${state.selectedSession ? html`
|
${state.selectedSession ? html`
|
||||||
<chat-view .sessionId=${state.selectedSession.id} .messages=${state.messages} .messageStart=${state.messagePageStart} .messageTotal=${state.messagePageTotal} .hasMore=${state.messagePageStart > 0} .loadingMore=${state.isLoadingEarlierMessages} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .status=${state.status} .activity=${state.activity} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
|
<chat-view .sessionId=${state.selectedSession.id} .messages=${state.messages} .messageStart=${state.messagePageStart} .messageTotal=${state.messagePageTotal} .hasMore=${state.messagePageStart > 0} .loadingMore=${state.isLoadingEarlierMessages} .isCompacting=${state.status?.isCompacting === true} .pendingMessageCount=${state.status?.pendingMessageCount ?? 0} .status=${state.status} .activity=${state.activity} .onLoadMore=${() => this.withChatPrependTransition(() => this.sessions.loadEarlierMessages())}></chat-view>
|
||||||
<prompt-editor .sessionId=${state.selectedSession.id} .cwd=${state.selectedWorkspace?.path} .disabled=${state.selectedSession.archived === true} .canSteer=${state.status?.isStreaming === true} .isCompacting=${state.status?.isCompacting === true} .canStop=${state.status?.isStreaming === true || state.status?.isBashRunning === true || state.status?.isCompacting === true} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp") => this.sessions.send(text, streamingBehavior)} .onStop=${() => this.sessions.stopActiveWork()}></prompt-editor>
|
<prompt-editor .sessionId=${state.selectedSession.id} .cwd=${state.selectedWorkspace?.path} .disabled=${state.selectedSession.archived === true} .canSteer=${state.status?.isStreaming === true} .isCompacting=${state.status?.isCompacting === true} .canStop=${state.status?.isStreaming === true || state.status?.isBashRunning === true || state.status?.isCompacting === true} .onSend=${(text: string, streamingBehavior?: "steer" | "followUp") => this.sessions.send(text, streamingBehavior)} .onStop=${() => this.sessions.stopActiveWork()}></prompt-editor>
|
||||||
<status-bar .status=${state.status} .workspace=${state.selectedWorkspace}></status-bar>
|
<status-bar .status=${state.status} .workspace=${state.selectedWorkspace}></status-bar>
|
||||||
${state.commandDialog !== undefined ? html`<command-picker .title=${state.commandDialog.title} .options=${state.commandDialog.options} .onPick=${(value: string) => this.sessions.respondToCommand(state.commandDialog?.requestId ?? "", value)} .onCancel=${() => { this.sessions.cancelCommand(); }}></command-picker>` : null}
|
${state.commandDialog !== undefined ? html`<command-picker .title=${state.commandDialog.title} .options=${state.commandDialog.options} .onPick=${(value: string) => this.sessions.respondToCommand(state.commandDialog?.requestId ?? "", value)} .onCancel=${() => { this.sessions.cancelCommand(); }}></command-picker>` : null}
|
||||||
` : html`<div class="empty">Select or start a session.</div>`}
|
` : html`<div class="empty">Select or start a session.</div>`}
|
||||||
<div class="mobile-panel">${this.renderWorkspacePanel()}</div>
|
<div class="mobile-panel">${this.renderWorkspacePanel(true)}</div>
|
||||||
</main>
|
</main>
|
||||||
${this.renderWorkspacePanel()}
|
${this.renderWorkspacePanel()}
|
||||||
${state.actionPaletteOpen ? html`<action-palette .actions=${this.getActions()} .onRun=${(actionId: string) => { this.setState({ actionPaletteOpen: false }); this.runAction(actionId); }} .onCancel=${() => { this.setState({ actionPaletteOpen: false }); }}></action-palette>` : null}
|
${state.actionPaletteOpen ? html`<action-palette .actions=${this.getActions()} .onRun=${(actionId: string) => { this.setState({ actionPaletteOpen: false }); this.runAction(actionId); }} .onCancel=${() => { this.setState({ actionPaletteOpen: false }); }}></action-palette>` : null}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export class WorkspacePanel extends LitElement {
|
|||||||
@property({ attribute: false }) workspace: Workspace | undefined;
|
@property({ attribute: false }) workspace: Workspace | undefined;
|
||||||
@property() tool: QualifiedContributionId = "core:workspace.files";
|
@property() tool: QualifiedContributionId = "core:workspace.files";
|
||||||
@property({ attribute: false }) panels: QualifiedWorkspacePanelContribution[] = [];
|
@property({ attribute: false }) panels: QualifiedWorkspacePanelContribution[] = [];
|
||||||
|
@property({ type: Boolean }) hideToolTabs = false;
|
||||||
@property({ attribute: false }) fileTree: FileTreeEntry[] = [];
|
@property({ attribute: false }) fileTree: FileTreeEntry[] = [];
|
||||||
@property({ attribute: false }) expandedDirs: Record<string, FileTreeEntry[]> = {};
|
@property({ attribute: false }) expandedDirs: Record<string, FileTreeEntry[]> = {};
|
||||||
@property({ attribute: false }) selectedFilePath: string | undefined;
|
@property({ attribute: false }) selectedFilePath: string | undefined;
|
||||||
@@ -33,11 +34,13 @@ export class WorkspacePanel extends LitElement {
|
|||||||
const selectedPanel = visiblePanels.find((panel) => panel.id === this.tool) ?? visiblePanels[0];
|
const selectedPanel = visiblePanels.find((panel) => panel.id === this.tool) ?? visiblePanels[0];
|
||||||
return html`
|
return html`
|
||||||
<header>
|
<header>
|
||||||
|
${this.hideToolTabs ? null : html`
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
${visiblePanels.map((panel) => html`
|
${visiblePanels.map((panel) => html`
|
||||||
<button class=${selectedPanel?.id === panel.id ? "selected" : ""} @click=${() => { this.onSelectTool(panel.id); }}>${panel.title}</button>
|
<button class=${selectedPanel?.id === panel.id ? "selected" : ""} @click=${() => { this.onSelectTool(panel.id); }}>${panel.title}</button>
|
||||||
`)}
|
`)}
|
||||||
</div>
|
</div>
|
||||||
|
`}
|
||||||
<small title=${workspace.path}>${workspace.label}</small>
|
<small title=${workspace.path}>${workspace.label}</small>
|
||||||
</header>
|
</header>
|
||||||
${selectedPanel === undefined ? html`<section class="empty">No workspace panels registered.</section>` : selectedPanel.render(this.createPanelContext(workspace))}
|
${selectedPanel === undefined ? html`<section class="empty">No workspace panels registered.</section>` : selectedPanel.render(this.createPanelContext(workspace))}
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ export const appStyles = css`
|
|||||||
project-list, workspace-list { flex: 0 0 auto; max-height: 26%; overflow: auto; border-bottom: 1px solid #21262d; }
|
project-list, workspace-list { flex: 0 0 auto; max-height: 26%; overflow: auto; border-bottom: 1px solid #21262d; }
|
||||||
session-list { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
session-list { flex: 1 1 auto; min-height: 0; overflow: auto; }
|
||||||
main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
main { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
|
||||||
.mobile-tabs { display: none; flex: 0 0 auto; gap: 6px; padding: 8px; border-bottom: 1px solid #30363d; }
|
.mobile-tabs { display: none; flex: 0 0 auto; gap: 6px; padding: 8px; border-bottom: 1px solid #30363d; overflow-x: auto; }
|
||||||
.mobile-panel { display: none; }
|
.mobile-navigation-tab, .mobile-navigation-panel, .mobile-panel { display: none; }
|
||||||
.mobile-tabs button.selected { border-color: #58a6ff; background: #0d2847; }
|
.mobile-tabs button.selected { border-color: #58a6ff; background: #0d2847; }
|
||||||
workspace-panel { min-width: 0; min-height: 0; border-left: 1px solid #30363d; overflow: hidden; }
|
workspace-panel { min-width: 0; min-height: 0; border-left: 1px solid #30363d; overflow: hidden; }
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
@@ -42,10 +42,18 @@ export const appStyles = css`
|
|||||||
.mobile-tabs { display: flex; }
|
.mobile-tabs { display: flex; }
|
||||||
main.workspace-view chat-view, main.workspace-view prompt-editor, main.workspace-view status-bar,
|
main.workspace-view chat-view, main.workspace-view prompt-editor, main.workspace-view status-bar,
|
||||||
main.workspace-view .empty { display: none; }
|
main.workspace-view .empty { display: none; }
|
||||||
main.chat-view .mobile-panel { display: none; }
|
main.chat-view .mobile-panel, main.navigation-view .mobile-panel { display: none; }
|
||||||
.mobile-panel { flex: 1 1 auto; min-height: 0; display: flex; }
|
.mobile-panel { flex: 1 1 auto; min-height: 0; display: flex; }
|
||||||
.mobile-panel workspace-panel { flex: 1 1 auto; border-left: 0; }
|
.mobile-panel workspace-panel { flex: 1 1 auto; border-left: 0; }
|
||||||
}
|
}
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.shell { grid-template-columns: minmax(0, 1fr); }
|
||||||
|
aside { display: none; }
|
||||||
|
.mobile-navigation-tab { display: block; }
|
||||||
|
main.navigation-view chat-view, main.navigation-view prompt-editor, main.navigation-view status-bar,
|
||||||
|
main.navigation-view .empty { display: none; }
|
||||||
|
main.navigation-view .mobile-navigation-panel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
}
|
||||||
status-bar { flex: 0 0 auto; }
|
status-bar { flex: 0 0 auto; }
|
||||||
chat-view { flex: 1 1 auto; min-height: 0; overflow: hidden; }
|
chat-view { flex: 1 1 auto; min-height: 0; overflow: hidden; }
|
||||||
prompt-editor, chat-composer { flex: 0 0 auto; }
|
prompt-editor, chat-composer { flex: 0 0 auto; }
|
||||||
|
|||||||
Reference in New Issue
Block a user