feat: add Pi Web theme picker

This commit is contained in:
Federico Jaramillo Martinez
2026-05-18 21:56:35 +02:00
parent 545499aadd
commit b51d56c36c
15 changed files with 689 additions and 192 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Add theme tokens, a theme picker, and built-in current/docs-inspired themes for the Pi Web UI.
+39 -1
View File
@@ -5,7 +5,45 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Pi Web</title>
<style>
html, body { margin: 0; height: 100%; overflow: hidden; }
:root {
color-scheme: dark;
--pi-bg: #0d1117;
--pi-surface: #161b22;
--pi-surface-hover: #21262d;
--pi-terminal-bg: #05070a;
--pi-terminal-text: #e6edf3;
--pi-border: #30363d;
--pi-border-muted: #21262d;
--pi-text: #e6edf3;
--pi-text-secondary: #c9d1d9;
--pi-text-bright: #f0f6fc;
--pi-muted: #8b949e;
--pi-dim: #6e7681;
--pi-accent: #58a6ff;
--pi-accent-border: #2f81f7;
--pi-selection-bg: #0d2847;
--pi-success: #3fb950;
--pi-success-border: #238636;
--pi-success-bg: #0f1b12;
--pi-success-surface: #0f2a16;
--pi-success-ring: #3fb95055;
--pi-warning: #d29922;
--pi-warning-border: #6e5200;
--pi-warning-surface: #1f1a10;
--pi-danger: #ff7b72;
--pi-purple: #d2a8ff;
--pi-purple-border: #a371f7;
--pi-purple-surface: #21132f;
--pi-overlay: #0008;
--pi-shadow-soft: #0006;
--pi-shadow: #0008;
--pi-shadow-strong: #000b;
--pi-bg-overlay-soft: #0d1117dd;
--pi-bg-overlay: #0d1117e6;
--pi-success-bg-overlay: #0f1b12ee;
--pi-terminal-selection: #264f78;
}
html, body { margin: 0; height: 100%; overflow: hidden; background: var(--pi-bg); color: var(--pi-text); }
body { min-height: 100dvh; }
</style>
</head>
+2
View File
@@ -21,6 +21,7 @@ export interface AppState {
commandDialog: Extract<CommandResult, { type: "select" }> | undefined;
modelDialog: { title: string; options: CommandOption[]; selectedValue?: string } | undefined;
thinkingDialog: { title: string; options: CommandOption[]; selectedValue?: string } | undefined;
themeDialog: { title: string; options: CommandOption[]; selectedValue?: string } | undefined;
authDialog: AuthDialogState | undefined;
actionPaletteOpen: boolean;
projectDialogOpen: boolean;
@@ -67,6 +68,7 @@ export function initialAppState(): AppState {
commandDialog: undefined,
modelDialog: undefined,
thinkingDialog: undefined,
themeDialog: undefined,
authDialog: undefined,
actionPaletteOpen: false,
projectDialogOpen: false,
+10 -10
View File
@@ -146,19 +146,19 @@ export class AuthDialog extends LitElement {
static override styles = [commandPickerStyles, css`
.form { display: grid; gap: 12px; padding: 14px; overflow: auto; }
.form p { margin: 0; color: #c9d1d9; overflow-wrap: anywhere; }
.form a { color: #58a6ff; overflow-wrap: anywhere; }
.form code { border: 1px solid #30363d; border-radius: 4px; background: #161b22; padding: 1px 4px; }
label { color: #8b949e; }
.form p { margin: 0; color: var(--pi-text-secondary); overflow-wrap: anywhere; }
.form a { color: var(--pi-accent); overflow-wrap: anywhere; }
.form code { border: 1px solid var(--pi-border); border-radius: 4px; background: var(--pi-surface); padding: 1px 4px; }
label { color: var(--pi-muted); }
.actions { display: flex; justify-content: flex-end; gap: 8px; }
.actions button, .inline-options button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; }
.actions button.primary { border-color: #238636; background: #0f2a16; color: #3fb950; }
.actions button, .inline-options button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; }
.actions button.primary { border-color: var(--pi-success-border); background: var(--pi-success-surface); color: var(--pi-success); }
.actions button:disabled { opacity: .6; cursor: wait; }
.warning { color: #d29922; }
.error-text { color: #ff7b72; }
.progress { margin: 0; padding-left: 18px; color: #8b949e; }
.warning { color: var(--pi-warning); }
.error-text { color: var(--pi-danger); }
.progress { margin: 0; padding-left: 18px; color: var(--pi-muted); }
.inline-options { display: grid; gap: 8px; }
em { color: #3fb950; font-style: normal; font-size: 12px; }
em { color: var(--pi-success); font-style: normal; font-size: 12px; }
`];
}
+5 -5
View File
@@ -70,8 +70,8 @@ export class CodeViewer extends LitElement {
const viewerTheme = EditorView.theme({
"&": {
height: "100%",
color: "#e6edf3",
backgroundColor: "#0d1117",
color: "var(--pi-text)",
backgroundColor: "var(--pi-bg)",
fontSize: "12px",
},
".cm-scroller": {
@@ -79,9 +79,9 @@ const viewerTheme = EditorView.theme({
lineHeight: "1.45",
},
".cm-gutters": {
backgroundColor: "#0d1117",
color: "#6e7681",
borderRight: "1px solid #21262d",
backgroundColor: "var(--pi-bg)",
color: "var(--pi-dim)",
borderRight: "1px solid var(--pi-border-muted)",
},
".cm-activeLineGutter": {
backgroundColor: "transparent",
+43
View File
@@ -12,7 +12,9 @@ import { WorkspaceController } from "../controllers/workspaceController";
import { KeyboardShortcutDispatcher } from "../keyboardShortcuts";
import { RealtimeSocket } from "../sessionSocket";
import type { QualifiedContributionId, QualifiedWorkspacePanelContribution, PluginRuntimeContext, WorkspacePanelContext } from "../plugins/types";
import { DEFAULT_THEME_ID, applyPiWebTheme, readStoredThemeId } from "../theme";
import { corePlugin } from "../plugins/core";
import { themePackPlugin } from "../plugins/themes";
import { loadExternalPlugins } from "../plugins/external";
import { PluginRegistry } from "../plugins/registry";
import { queryNamespace, readNamespacedString } from "../namespacedQueryArgs";
@@ -77,6 +79,8 @@ export class PiWebApp extends LitElement {
private readonly mobileNavigationMedia = typeof window !== "undefined" && "matchMedia" in window ? window.matchMedia("(max-width: 760px)") : undefined;
private terminalAutoStartWorkspaceId: string | undefined;
private readonly plugins = createPluginRegistry();
private preferredThemeId: QualifiedContributionId = readStoredThemeId() ?? DEFAULT_THEME_ID;
@state() private activeThemeId: QualifiedContributionId = DEFAULT_THEME_ID;
@state() private isMobileNavigationLayout = this.mobileNavigationMedia?.matches ?? false;
@state() private expandedMobileNavigationSection: NavigationSection | "none" | undefined;
private readonly onPopState = () => void this.withChatScrollTransition(() => this.restoreRoute(false));
@@ -101,6 +105,7 @@ export class PiWebApp extends LitElement {
document.addEventListener("visibilitychange", this.onVisibilityChange);
window.addEventListener("keydown", this.onKeyDown);
this.mobileNavigationMedia?.addEventListener("change", this.onMobileNavigationMediaChange);
this.applyPreferredTheme(false);
this.connectRealtime();
void this.loadExternalPlugins();
void this.loadProjectsAndRestoreRoute();
@@ -387,6 +392,7 @@ export class PiWebApp extends LitElement {
private async loadExternalPlugins(): Promise<void> {
try {
for (const registration of await loadExternalPlugins()) this.plugins.register(registration);
this.applyPreferredTheme(false);
this.requestUpdate();
} catch (error) {
console.warn("Failed to load external Pi Web plugins", error);
@@ -401,6 +407,7 @@ export class PiWebApp extends LitElement {
addProject: () => { this.setState({ projectDialogOpen: true }); },
configureAuth: () => this.auth.openLogin(),
logoutAuth: () => this.auth.openLogout(),
openThemePicker: () => { this.openThemeDialog(); },
selectMainView: (view) => { this.selectMainView(view); },
selectWorkspaceTool: (tool) => { this.openWorkspaceTool(tool); },
refreshFiles: () => this.files.refreshFiles(),
@@ -441,6 +448,40 @@ export class PiWebApp extends LitElement {
await this.sessions.setModel(value.slice(0, slash), value.slice(slash + 1));
}
private openThemeDialog() {
const themes = this.plugins.getThemes();
this.setState({
themeDialog: {
title: "Select Theme",
selectedValue: this.activeThemeId,
options: themes.map((theme) => ({
value: theme.id,
label: `${theme.name}${theme.id === this.activeThemeId ? " ✓ current" : ""}`,
description: theme.description === undefined ? theme.colorScheme : `${theme.colorScheme} · ${theme.description}`,
})),
},
});
}
private pickTheme(value: string) {
const theme = this.plugins.getThemes().find((candidate) => candidate.id === value);
this.setState({ themeDialog: undefined });
if (theme === undefined) return;
this.preferredThemeId = theme.id;
this.activeThemeId = theme.id;
applyPiWebTheme(theme);
}
private applyPreferredTheme(persist: boolean): void {
const themes = this.plugins.getThemes();
const theme = themes.find((candidate) => candidate.id === this.preferredThemeId)
?? themes.find((candidate) => candidate.id === DEFAULT_THEME_ID)
?? themes[0];
if (theme === undefined) return;
this.activeThemeId = theme.id;
applyPiWebTheme(theme, { persist });
}
private async openThinkingDialog() {
const levels = await this.sessions.listThinkingLevels();
const current = this.state.status?.thinkingLevel ?? "off";
@@ -491,6 +532,7 @@ export class PiWebApp extends LitElement {
${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.projectDialogOpen ? html`<project-dialog .onSubmit=${(path: string, create: boolean) => this.projects.addProject(path, create)} .onCancel=${() => { this.setState({ projectDialogOpen: false }); }}></project-dialog>` : null}
${state.themeDialog !== undefined ? html`<command-picker title=${state.themeDialog.title} .options=${state.themeDialog.options} .selectedValue=${state.themeDialog.selectedValue} .onPick=${(value: string) => { this.pickTheme(value); }} .onCancel=${() => { this.setState({ themeDialog: undefined }); }}></command-picker>` : null}
</div>
`;
}
@@ -501,6 +543,7 @@ export class PiWebApp extends LitElement {
function createPluginRegistry(): PluginRegistry {
const registry = new PluginRegistry();
registry.register({ id: "core", plugin: corePlugin });
registry.register({ id: "themes", plugin: themePackPlugin });
return registry;
}
+15 -15
View File
@@ -123,22 +123,22 @@ export class ProjectDialog extends LitElement {
}
static override styles = css`
:host { position: fixed; inset: 0; z-index: 30; color: #e6edf3; font: 14px system-ui, sans-serif; }
.backdrop { display: grid; place-items: start center; width: 100%; height: 100%; padding-top: min(12vh, 90px); box-sizing: border-box; background: #0008; }
section { width: min(720px, calc(100vw - 40px)); max-height: min(700px, calc(100vh - 40px)); display: flex; flex-direction: column; border: 1px solid #30363d; border-radius: 12px; background: #0d1117; box-shadow: 0 20px 60px #000b; overflow: hidden; }
header, footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid #30363d; }
footer { border-top: 1px solid #30363d; border-bottom: 0; justify-content: end; }
:host { position: fixed; inset: 0; z-index: 30; color: var(--pi-text); font: 14px system-ui, sans-serif; }
.backdrop { display: grid; place-items: start center; width: 100%; height: 100%; padding-top: min(12vh, 90px); box-sizing: border-box; background: var(--pi-overlay); }
section { width: min(720px, calc(100vw - 40px)); max-height: min(700px, calc(100vh - 40px)); display: flex; flex-direction: column; border: 1px solid var(--pi-border); border-radius: 12px; background: var(--pi-bg); box-shadow: 0 20px 60px var(--pi-shadow-strong); overflow: hidden; }
header, footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid var(--pi-border); }
footer { border-top: 1px solid var(--pi-border); border-bottom: 0; justify-content: end; }
.body { display: grid; gap: 12px; padding: 12px; min-height: 0; }
label { display: grid; gap: 6px; color: #8b949e; }
input[type="text"], input:not([type]) { box-sizing: border-box; width: 100%; border: 1px solid #30363d; border-radius: 8px; background: #0d1117; color: #e6edf3; padding: 9px; font: 14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.check { display: flex; grid-template-columns: auto 1fr; align-items: center; color: #e6edf3; }
.suggestions { min-height: 90px; max-height: 320px; overflow: auto; border: 1px solid #30363d; border-radius: 8px; background: #161b22; }
.suggestions button { display: block; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 0; border-bottom: 1px solid #30363d; border-radius: 0; background: transparent; color: #e6edf3; padding: 8px 10px; text-align: left; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.suggestions button.selected, .suggestions button:hover { background: #0d2847; }
.hint { padding: 12px; color: #8b949e; }
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
header button { border: 0; background: transparent; color: #8b949e; font-size: 22px; padding: 0 8px; }
.primary { border-color: #238636; background: #238636; }
label { display: grid; gap: 6px; color: var(--pi-muted); }
input[type="text"], input:not([type]) { box-sizing: border-box; width: 100%; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); color: var(--pi-text); padding: 9px; font: 14px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.check { display: flex; grid-template-columns: auto 1fr; align-items: center; color: var(--pi-text); }
.suggestions { min-height: 90px; max-height: 320px; overflow: auto; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); }
.suggestions button { display: block; width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border: 0; border-bottom: 1px solid var(--pi-border); border-radius: 0; background: transparent; color: var(--pi-text); padding: 8px 10px; text-align: left; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.suggestions button.selected, .suggestions button:hover { background: var(--pi-selection-bg); }
.hint { padding: 12px; color: var(--pi-muted); }
button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; }
header button { border: 0; background: transparent; color: var(--pi-muted); font-size: 22px; padding: 0 8px; }
.primary { border-color: var(--pi-success-border); background: var(--pi-success-border); }
button:disabled { opacity: .5; cursor: not-allowed; }
`;
}
+45 -15
View File
@@ -1,16 +1,15 @@
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators.js";
import { Terminal, type ITerminalOptions } from "@xterm/xterm";
import { Terminal, type ITerminalOptions, type ITheme } from "@xterm/xterm";
import { FitAddon, type ITerminalDimensions } from "@xterm/addon-fit";
import "@xterm/xterm/css/xterm.css";
import { terminalSocket, terminalsApi, type TerminalInfo, type Workspace } from "../api";
const TERMINAL_OPTIONS: ITerminalOptions = {
const TERMINAL_OPTIONS_BASE: ITerminalOptions = {
cursorBlink: true,
convertEol: true,
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
fontSize: 13,
theme: { background: "#05070a", foreground: "#e6edf3", cursor: "#58a6ff", selectionBackground: "#264f78" },
};
const DEFAULT_TERMINAL_SIZE: TerminalSize = { cols: 100, rows: 30 };
@@ -31,10 +30,17 @@ export class TerminalPanel extends LitElement {
private socket: WebSocket | undefined;
private resizeObserver: ResizeObserver | undefined;
private intersectionObserver: IntersectionObserver | undefined;
private themeObserver: MutationObserver | undefined;
private suppressTerminalInput = false;
private observedCwd: string | undefined;
private loadedCwd: string | undefined;
override connectedCallback(): void {
super.connectedCallback();
this.themeObserver = new MutationObserver(() => { this.applyTerminalTheme(); });
this.themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "style", "data-theme"] });
}
override firstUpdated(): void {
this.intersectionObserver = new IntersectionObserver((entries) => {
this.visible = entries[0]?.isIntersecting === true;
@@ -45,6 +51,8 @@ export class TerminalPanel extends LitElement {
override disconnectedCallback(): void {
this.intersectionObserver?.disconnect();
this.intersectionObserver = undefined;
this.themeObserver?.disconnect();
this.themeObserver = undefined;
this.disposeTerminalView();
super.disconnectedCallback();
}
@@ -126,7 +134,7 @@ export class TerminalPanel extends LitElement {
private ensureTerminalView(): void {
const workspace = this.workspace;
if (!this.visible || this.terminal !== undefined || this.selectedId === undefined || this.terminalHost === undefined || workspace === undefined) return;
const terminal = new Terminal(TERMINAL_OPTIONS);
const terminal = new Terminal(terminalOptions(this));
const fitAddon = new FitAddon();
terminal.loadAddon(fitAddon);
terminal.open(this.terminalHost);
@@ -205,7 +213,7 @@ export class TerminalPanel extends LitElement {
if (currentSize !== undefined) return currentSize;
if (this.terminal !== undefined || this.terminalHost === undefined) return undefined;
const measuringTerminal = new Terminal(TERMINAL_OPTIONS);
const measuringTerminal = new Terminal(terminalOptions(this));
const measuringFitAddon = new FitAddon();
measuringTerminal.loadAddon(measuringFitAddon);
measuringTerminal.open(this.terminalHost);
@@ -214,6 +222,10 @@ export class TerminalPanel extends LitElement {
return size;
}
private applyTerminalTheme(): void {
if (this.terminal !== undefined) this.terminal.options.theme = terminalTheme(this);
}
private send(message: { type: "input"; data: string } | { type: "resize"; cols: number; rows: number }): void {
if (this.socket?.readyState === WebSocket.OPEN) this.socket.send(JSON.stringify(message));
}
@@ -249,21 +261,21 @@ export class TerminalPanel extends LitElement {
static override styles = css`
:host { flex: 1 1 auto; min-height: 0; display: flex; }
.terminal-shell { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: #05070a; }
.terminal-tabs { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; padding: 6px; border-bottom: 1px solid #21262d; background: #0d1117; overflow: auto; }
button { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 180px; border: 1px solid #30363d; border-radius: 7px; background: #161b22; color: #e6edf3; padding: 5px 7px; cursor: pointer; }
button.selected { border-color: #58a6ff; background: #0d2847; }
button.new { flex: 0 0 auto; color: #8b949e; }
.terminal-shell { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: var(--pi-terminal-bg); }
.terminal-tabs { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; padding: 6px; border-bottom: 1px solid var(--pi-border-muted); background: var(--pi-bg); overflow: auto; }
button { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 180px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 7px; cursor: pointer; }
button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
button.new { flex: 0 0 auto; color: var(--pi-muted); }
button span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
button small { color: #8b949e; font-size: 14px; line-height: 1; }
button small:hover { color: #ff7b72; }
button small { color: var(--pi-muted); font-size: 14px; line-height: 1; }
button small:hover { color: var(--pi-danger); }
button:disabled { opacity: .5; cursor: not-allowed; }
.terminal-host { flex: 1 1 auto; min-height: 0; padding: 6px; box-sizing: border-box; overflow: hidden; }
.terminal-host .xterm { height: 100%; cursor: text; position: relative; user-select: none; }
.terminal-host .xterm.focus, .terminal-host .xterm:focus { outline: none; }
.terminal-host .xterm-helpers { position: absolute; top: 0; z-index: 5; }
.terminal-host .xterm-helper-textarea { position: absolute !important; left: -9999em !important; top: 0 !important; width: 0 !important; height: 0 !important; min-width: 0 !important; min-height: 0 !important; padding: 0 !important; border: 0 !important; margin: 0 !important; opacity: 0 !important; z-index: -5 !important; white-space: nowrap !important; overflow: hidden !important; resize: none !important; outline: 0 !important; appearance: none !important; }
.terminal-host .xterm-viewport { position: absolute; inset: 0; overflow-y: scroll; cursor: default; background-color: #05070a; }
.terminal-host .xterm-viewport { position: absolute; inset: 0; overflow-y: scroll; cursor: default; background-color: var(--pi-terminal-bg); }
.terminal-host .xterm-screen { position: relative; }
.terminal-host .xterm-screen canvas { position: absolute; left: 0; top: 0; }
.terminal-host .xterm-char-measure-element { display: inline-block; visibility: hidden; position: absolute; top: 0; left: -9999em; line-height: normal; }
@@ -272,8 +284,8 @@ export class TerminalPanel extends LitElement {
.terminal-host .xterm-accessibility-tree { font-family: monospace; user-select: text; white-space: pre; }
.terminal-host .xterm-accessibility-tree > div { transform-origin: left; width: fit-content; }
.terminal-host .live-region { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
.error { flex: 0 0 auto; margin: 0; padding: 8px; color: #ff7b72; border-bottom: 1px solid #30363d; background: #161b22; }
.muted { margin: 10px; color: #8b949e; }
.error { flex: 0 0 auto; margin: 0; padding: 8px; color: var(--pi-danger); border-bottom: 1px solid var(--pi-border); background: var(--pi-surface); }
.muted { margin: 10px; color: var(--pi-muted); }
.xterm { height: 100%; }
`;
}
@@ -312,6 +324,24 @@ async function socketDataToString(data: unknown): Promise<string> {
return String(data);
}
function terminalOptions(element: HTMLElement): ITerminalOptions {
return { ...TERMINAL_OPTIONS_BASE, theme: terminalTheme(element) };
}
function terminalTheme(element: HTMLElement): ITheme {
return {
background: themeColor(element, "--pi-terminal-bg", "#05070a"),
foreground: themeColor(element, "--pi-terminal-text", "#e6edf3"),
cursor: themeColor(element, "--pi-accent", "#58a6ff"),
selectionBackground: themeColor(element, "--pi-terminal-selection", "#264f78"),
};
}
function themeColor(element: HTMLElement, name: string, fallback: string): string {
const value = getComputedStyle(element).getPropertyValue(name).trim();
return value === "" ? fallback : value;
}
function terminalSizeFromDimensions(dimensions: ITerminalDimensions | undefined): TerminalSize | undefined {
if (dimensions === undefined || !isValidTerminalSize(dimensions.cols, dimensions.rows)) return undefined;
return { cols: Math.floor(dimensions.cols), rows: Math.floor(dimensions.rows) };
+146 -144
View File
@@ -30,18 +30,18 @@ export interface CompletionItem {
}
export const appStyles = css`
:host { display: block; height: 100dvh; box-sizing: border-box; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); color: #e6edf3; background: #0d1117; font: 14px system-ui, sans-serif; }
:host { display: block; height: 100dvh; box-sizing: border-box; padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); color: var(--pi-text); background: var(--pi-bg); font: 14px system-ui, sans-serif; }
.shell { display: grid; grid-template-columns: 340px minmax(420px, 1fr) minmax(360px, 42vw); height: 100%; min-height: 0; }
aside { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid #30363d; overflow: hidden; }
header { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid #30363d; }
project-list, workspace-list { flex: 0 0 auto; max-height: 26%; overflow: auto; border-bottom: 1px solid #21262d; }
aside { display: flex; flex-direction: column; min-height: 0; border-right: 1px solid var(--pi-border); overflow: hidden; }
header { flex: 0 0 auto; display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px; border-bottom: 1px solid var(--pi-border); }
project-list, workspace-list { flex: 0 0 auto; max-height: 26%; overflow: auto; border-bottom: 1px solid var(--pi-border-muted); }
session-list { flex: 1 1 auto; min-height: 0; overflow: auto; }
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; overflow-x: auto; }
.mobile-tabs { display: none; flex: 0 0 auto; gap: 6px; padding: 8px; border-bottom: 1px solid var(--pi-border); overflow-x: auto; }
.mobile-navigation-tab, .mobile-navigation-panel { display: none; }
.mobile-tabs button.selected { border-color: #58a6ff; background: #0d2847; }
.tab-badge { display: inline-block; min-width: 14px; margin-left: 4px; border: 1px solid #238636; border-radius: 999px; background: #0f2a16; color: #3fb950; padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
workspace-panel { min-width: 0; min-height: 0; border-left: 1px solid #30363d; overflow: hidden; }
.mobile-tabs button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
.tab-badge { display: inline-block; min-width: 14px; margin-left: 4px; 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; }
workspace-panel { min-width: 0; min-height: 0; border-left: 1px solid var(--pi-border); overflow: hidden; }
@media (max-width: 1180px) {
.shell { grid-template-columns: 340px minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr); }
aside { grid-row: 1 / 3; }
@@ -72,42 +72,42 @@ export const appStyles = css`
status-bar { flex: 0 0 auto; }
chat-view { flex: 1 1 auto; min-height: 0; overflow: hidden; }
prompt-editor, chat-composer { flex: 0 0 auto; }
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
.empty { margin: auto; color: #8b949e; }
.error { padding: 10px 16px; border-bottom: 1px solid #30363d; color: #ff7b72; }
button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; }
.empty { margin: auto; color: var(--pi-muted); }
.error { padding: 10px 16px; border-bottom: 1px solid var(--pi-border); color: var(--pi-danger); }
`;
export const workspacePanelStyles = css`
:host { display: flex; flex-direction: column; min-height: 0; color: #e6edf3; background: #0d1117; font: 13px system-ui, sans-serif; }
header { flex: 0 0 auto; display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid #30363d; }
:host { display: flex; flex-direction: column; min-height: 0; color: var(--pi-text); background: var(--pi-bg); font: 13px system-ui, sans-serif; }
header { flex: 0 0 auto; display: flex; justify-content: space-between; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border); }
.tabs { display: flex; gap: 6px; }
button { display: inline-flex; align-items: center; gap: 5px; border: 1px solid #30363d; border-radius: 7px; background: #161b22; color: #e6edf3; padding: 5px 7px; cursor: pointer; }
button.selected { border-color: #58a6ff; background: #0d2847; }
.tab-badge { display: inline-block; min-width: 14px; border: 1px solid #238636; border-radius: 999px; background: #0f2a16; color: #3fb950; padding: 0 5px; font-size: 11px; line-height: 16px; text-align: center; }
button { display: inline-flex; align-items: center; gap: 5px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 7px; cursor: pointer; }
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; }
small, .muted { color: #8b949e; }
small, .muted { color: var(--pi-muted); }
header small { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@media (max-width: 1180px) { .tabs { display: none; } }
.workspace-label { min-width: 0; display: inline-flex; align-items: baseline; gap: 5px; max-width: 100%; overflow: hidden; white-space: nowrap; }
.workspace-label-base, .workspace-label-item, .workspace-label-render { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: #8b949e; }
.workspace-label-link { color: #58a6ff; text-decoration: none; }
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: var(--pi-muted); }
.workspace-label-link { color: var(--pi-accent); text-decoration: none; }
.workspace-label-link:hover, .workspace-label-link:focus { text-decoration: underline; }
.toolbar { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid #21262d; }
.toolbar { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border-muted); }
.toolbar button { margin-left: auto; }
.stale { border: 1px solid #6e5200; border-radius: 999px; color: #d29922; padding: 1px 6px; font-size: 12px; }
.stale { border: 1px solid var(--pi-warning-border); border-radius: 999px; color: var(--pi-warning); padding: 1px 6px; font-size: 12px; }
.split { flex: 1 1 auto; min-height: 0; display: grid; grid-template-rows: minmax(160px, 34%) minmax(0, 1fr); }
.list { min-height: 0; overflow: auto; border-bottom: 1px solid #30363d; padding: 6px; }
.list { min-height: 0; overflow: auto; border-bottom: 1px solid var(--pi-border); padding: 6px; }
.row { display: grid; grid-template-columns: 18px minmax(0, 1fr); gap: 4px; width: 100%; border: 0; border-radius: 5px; background: transparent; text-align: left; padding: 4px 6px 4px calc(6px + var(--depth, 0) * 14px); }
.row:hover, .row.selected { background: #0d2847; }
.row:hover, .row.selected { background: var(--pi-selection-bg); }
.row span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.summary { margin: 4px 6px 8px; color: #8b949e; }
.summary { margin: 4px 6px 8px; color: var(--pi-muted); }
.viewer { min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
.diffs { flex: 1 1 auto; min-height: 0; overflow: auto; display: grid; grid-template-rows: minmax(120px, 1fr) minmax(120px, 1fr); }
.diffs.single { grid-template-rows: minmax(0, 1fr); }
.diff-section { min-height: 0; display: flex; flex-direction: column; border-bottom: 1px solid #30363d; }
.diff-section { min-height: 0; display: flex; flex-direction: column; border-bottom: 1px solid var(--pi-border); }
.diff-section:last-child { border-bottom: 0; }
.viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid #21262d; background: #0d1117; }
.viewer-header { position: sticky; top: 0; display: flex; justify-content: space-between; gap: 8px; padding: 8px; border-bottom: 1px solid var(--pi-border-muted); background: var(--pi-bg); }
.viewer-header strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
code-viewer { flex: 1 1 auto; min-height: 0; }
pre { margin: 0; padding: 10px; overflow: auto; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; }
@@ -115,95 +115,97 @@ export const workspacePanelStyles = css`
`;
export const listStyles = css`
:host { display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
:host { display: block; color: var(--pi-text); font: 14px system-ui, sans-serif; }
:host([collapsed]) { flex: 0 0 auto; min-height: auto; overflow: hidden; }
section { padding: 10px; }
h2 { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: 0 0 8px; color: #8b949e; font-size: 12px; text-transform: uppercase; }
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
h2 { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin: 0 0 8px; color: var(--pi-muted); font-size: 12px; text-transform: uppercase; }
button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; }
section > button { display: block; width: 100%; text-align: left; margin: 6px 0; }
.subheading { margin-top: 14px; }
.section-toggle { display: flex; flex: 1 1 auto; min-width: 0; align-items: center; justify-content: space-between; gap: 8px; width: 100%; border: 0; background: transparent; color: inherit; padding: 0; font: inherit; text-transform: inherit; }
.section-toggle span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.section-toggle small { display: inline; color: inherit; font-size: inherit; }
.action-row { position: relative; display: grid; grid-template-columns: minmax(0, 1fr) auto; margin: 6px 0; cursor: pointer; }
.action-row:focus-visible { outline: 2px solid #58a6ff; outline-offset: 2px; border-radius: 8px; }
.action-row.selected .action-main, .action-row.selected .action-menu-toggle { border-color: #58a6ff; background: #0d2847; }
.action-row.archived .action-main { color: #8b949e; }
.action-main { box-sizing: border-box; min-width: 0; width: 100%; border: 1px solid #30363d; border-top-right-radius: 0; border-bottom-right-radius: 0; border-top-left-radius: 8px; border-bottom-left-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px 7px calc(9px + var(--depth, 0) * 16px); text-align: left; }
.action-row:not(.selected):hover .action-main { background: #21262d; }
.action-row:focus-visible { outline: 2px solid var(--pi-accent); outline-offset: 2px; border-radius: 8px; }
.action-row.selected .action-main, .action-row.selected .action-menu-toggle { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
.action-row.archived .action-main { color: var(--pi-muted); }
.action-main { box-sizing: border-box; min-width: 0; width: 100%; border: 1px solid var(--pi-border); border-top-right-radius: 0; border-bottom-right-radius: 0; border-top-left-radius: 8px; border-bottom-left-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px 7px calc(9px + var(--depth, 0) * 16px); text-align: left; }
.action-row:not(.selected):hover .action-main { background: var(--pi-surface-hover); }
.workspace-row .action-main { border-radius: 8px; }
.tree-marker { color: #6e7681; margin-right: 5px; }
.badge { display: inline-block; margin-left: 5px; border: 1px solid #30363d; border-radius: 999px; color: #8b949e; padding: 0 5px; font-size: 11px; font-weight: 400; }
.tree-marker { color: var(--pi-dim); margin-right: 5px; }
.badge { display: inline-block; margin-left: 5px; border: 1px solid var(--pi-border); border-radius: 999px; color: var(--pi-muted); padding: 0 5px; font-size: 11px; font-weight: 400; }
.action-menu { position: relative; align-self: stretch; }
.action-menu-toggle { display: grid; place-items: center; height: 100%; min-width: 32px; padding: 0; color: #8b949e; border-left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.action-menu-toggle:hover { color: #e6edf3; background: #21262d; }
.action-menu-panel { position: fixed; z-index: 50; min-width: 120px; padding: 4px; border: 1px solid #30363d; border-radius: 8px; background: #161b22; box-shadow: 0 8px 24px #0008; }
.action-menu-panel button { display: block; width: 100%; text-align: left; border: 0; background: transparent; color: #e6edf3; }
.action-menu-panel button:hover { background: #0d2847; }
button.selected { border-color: #58a6ff; background: #0d2847; }
.action-menu-toggle { display: grid; place-items: center; height: 100%; min-width: 32px; padding: 0; color: var(--pi-muted); border-left: 0; border-top-left-radius: 0; border-bottom-left-radius: 0; }
.action-menu-toggle:hover { color: var(--pi-text); background: var(--pi-surface-hover); }
.action-menu-panel { position: fixed; z-index: 50; min-width: 120px; padding: 4px; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); box-shadow: 0 8px 24px var(--pi-shadow); }
.action-menu-panel button { display: block; width: 100%; text-align: left; border: 0; background: transparent; color: var(--pi-text); }
.action-menu-panel button:hover { background: var(--pi-selection-bg); }
button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
button:disabled { opacity: .5; cursor: not-allowed; }
small { display: block; color: #8b949e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
small { display: block; color: var(--pi-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.workspace-label { min-width: 0; display: inline-flex; align-items: baseline; gap: 5px; max-width: 100%; overflow: hidden; white-space: nowrap; }
.workspace-label-base, .workspace-label-item, .workspace-label-render { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: #8b949e; }
.workspace-label-link { color: #58a6ff; text-decoration: none; }
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: var(--pi-muted); }
.workspace-label-link { color: var(--pi-accent); text-decoration: none; }
.workspace-label-link:hover, .workspace-label-link:focus { text-decoration: underline; }
`;
export const chatStyles = css`
:host { display: flex; flex-direction: column; min-height: 0; overflow: hidden; color: #e6edf3; font: 14px system-ui, sans-serif; }
:host { display: flex; flex-direction: column; min-height: 0; overflow: hidden; color: var(--pi-text); font: 14px system-ui, sans-serif; }
.chat-wrap { position: relative; flex: 1 1 auto; min-height: 0; overflow: hidden; }
.chat { height: 100%; min-height: 0; overflow: auto; overflow-anchor: none; padding: 16px 16px 64px; box-sizing: border-box; }
.scroll-marker { display: block; height: 0; overflow: hidden; pointer-events: none; }
.history-indicator { position: absolute; top: 10px; right: 18px; z-index: 2; display: grid; gap: 2px; max-width: min(320px, calc(100% - 36px)); border: 1px solid #30363d; border-radius: 8px; background: #0d1117dd; color: #8b949e; padding: 6px 8px; font-size: 12px; text-align: right; pointer-events: none; box-shadow: 0 8px 24px #0006; }
.activity-dock { position: absolute; left: 16px; right: 16px; bottom: 12px; z-index: 3; display: flex; align-items: center; gap: 8px; min-width: 0; box-sizing: border-box; border: 1px solid #30363d; border-radius: 999px; background: #0d1117e6; color: #8b949e; padding: 8px 12px; font-size: 13px; pointer-events: none; box-shadow: 0 8px 28px #0008; backdrop-filter: blur(6px); }
.activity-dock.active { border-color: #238636; color: #3fb950; background: #0f1b12ee; }
.history-indicator { position: absolute; top: 10px; right: 18px; z-index: 2; display: grid; gap: 2px; max-width: min(320px, calc(100% - 36px)); border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg-overlay-soft); color: var(--pi-muted); padding: 6px 8px; font-size: 12px; text-align: right; pointer-events: none; box-shadow: 0 8px 24px var(--pi-shadow-soft); }
.activity-dock { position: absolute; left: 16px; right: 16px; bottom: 12px; z-index: 3; display: flex; align-items: center; gap: 8px; min-width: 0; box-sizing: border-box; border: 1px solid var(--pi-border); border-radius: 999px; background: var(--pi-bg-overlay); color: var(--pi-muted); padding: 8px 12px; font-size: 13px; pointer-events: none; box-shadow: 0 8px 28px var(--pi-shadow); backdrop-filter: blur(6px); }
.activity-dock.active { border-color: var(--pi-success-border); color: var(--pi-success); background: var(--pi-success-bg-overlay); }
.activity-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dot { width: 8px; height: 8px; border-radius: 50%; background: currentColor; opacity: .45; flex: 0 0 auto; }
.activity-dock.active .dot { animation: pulse 1s ease-in-out infinite; opacity: 1; }
.msg { max-width: 100%; min-width: 0; box-sizing: border-box; margin: 0 0 14px; padding: 12px; border: 1px solid #30363d; border-radius: 10px; background: #161b22; overflow: hidden; }
.msg.user { border-color: #2f81f7; background: #0d2847; }
.msg.tool { border-color: #6e5200; background: #1f1a10; color: #d29922; }
.msg.system { color: #ff7b72; }
.msg.bash { border-color: #3fb950; background: #0f1b12; }
.msg.skill { border-color: #a371f7; background: #21132f; }
.msg.event-group { padding: 0; border-color: #30363d; background: #0d1117; color: #8b949e; }
.msg.event-group > summary { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: #8b949e; }
.msg { max-width: 100%; min-width: 0; box-sizing: border-box; margin: 0 0 14px; padding: 12px; border: 1px solid var(--pi-border); border-radius: 10px; background: var(--pi-surface); overflow: hidden; }
.msg.user { border-color: var(--pi-accent-border); background: var(--pi-selection-bg); }
.msg.tool { border-color: var(--pi-warning-border); background: var(--pi-warning-surface); color: var(--pi-warning); }
.msg.system { color: var(--pi-danger); }
.msg.bash { border-color: var(--pi-success); background: var(--pi-success-bg); }
.msg.skill { border-color: var(--pi-purple-border); background: var(--pi-purple-surface); }
.msg.event-group { padding: 0; border-color: var(--pi-border); background: var(--pi-bg); color: var(--pi-muted); }
.msg.event-group.live { border-color: var(--pi-success-border); background: var(--pi-success-bg); }
.msg.event-group > summary { display: flex; align-items: center; gap: 8px; padding: 8px 12px; color: var(--pi-muted); }
.msg.event-group.live > summary { color: var(--pi-success); }
.msg.event-group > summary .label { margin: 0; }
.group-body { padding: 0 12px 12px; }
.group-msg { max-width: 100%; min-width: 0; box-sizing: border-box; padding: 10px 0; border-top: 1px solid #21262d; color: #e6edf3; overflow: hidden; }
.group-msg.tool { color: #d29922; }
.group-msg.system { color: #ff7b72; }
.group-msg.bash { color: #3fb950; }
.history-boundary { display: grid; gap: 3px; justify-items: center; margin: 0 0 14px; color: #8b949e; font-size: 12px; text-align: center; }
.history-load-button { border: 1px solid #30363d; border-radius: 999px; background: #161b22; color: #c9d1d9; padding: 5px 12px; font: 12px system-ui, sans-serif; cursor: pointer; }
.history-load-button:hover, .history-load-button:focus { border-color: #58a6ff; color: #f0f6fc; }
.group-msg { max-width: 100%; min-width: 0; box-sizing: border-box; padding: 10px 0; border-top: 1px solid var(--pi-border-muted); color: var(--pi-text); overflow: hidden; }
.group-msg.tool { color: var(--pi-warning); }
.group-msg.system { color: var(--pi-danger); }
.group-msg.bash { color: var(--pi-success); }
.history-boundary { display: grid; gap: 3px; justify-items: center; margin: 0 0 14px; color: var(--pi-muted); font-size: 12px; text-align: center; }
.history-load-button { border: 1px solid var(--pi-border); border-radius: 999px; background: var(--pi-surface); color: var(--pi-text-secondary); padding: 5px 12px; font: 12px system-ui, sans-serif; cursor: pointer; }
.history-load-button:hover, .history-load-button:focus { border-color: var(--pi-accent); color: var(--pi-text-bright); }
.history-load-button:disabled { cursor: default; opacity: .55; }
.queued-messages { max-width: 100%; min-width: 0; box-sizing: border-box; display: grid; gap: 8px; margin: 0 0 14px; padding: 12px; border: 1px solid #6e5200; border-radius: 10px; background: #1f1a10; color: #e6edf3; overflow: hidden; }
.queued-messages { max-width: 100%; min-width: 0; box-sizing: border-box; display: grid; gap: 8px; margin: 0 0 14px; padding: 12px; border: 1px solid var(--pi-warning-border); border-radius: 10px; background: var(--pi-warning-surface); color: var(--pi-text); overflow: hidden; }
.queued-header { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; }
.queued-header strong { color: #d29922; }
.queued-header small { color: #8b949e; }
.queued-message { display: grid; gap: 4px; padding-top: 8px; border-top: 1px solid #30363d; }
.queued-header strong { color: var(--pi-warning); }
.queued-header small { color: var(--pi-muted); }
.queued-message { display: grid; gap: 4px; padding-top: 8px; border-top: 1px solid var(--pi-border); }
.queued-message:first-of-type { padding-top: 0; border-top: 0; }
.queued-kind { color: #8b949e; font-size: 12px; text-transform: uppercase; }
.session-activity { max-width: 100%; min-width: 0; box-sizing: border-box; display: grid; gap: 4px; margin: 0 0 14px; padding: 12px; border: 1px solid #30363d; border-radius: 10px; background: #161b22; color: #e6edf3; overflow: hidden; }
.session-activity.compacting { border-color: #a371f7; background: #21132f; }
.session-activity.receiving { border-color: #238636; background: #0f1b12; }
.session-activity strong { color: #d2a8ff; }
.session-activity.receiving strong { color: #3fb950; }
.session-activity span, .session-activity small { color: #8b949e; }
.history-boundary small { color: #6e7681; }
.queued-kind { color: var(--pi-muted); font-size: 12px; text-transform: uppercase; }
.session-activity { max-width: 100%; min-width: 0; box-sizing: border-box; display: grid; gap: 4px; margin: 0 0 14px; padding: 12px; border: 1px solid var(--pi-border); border-radius: 10px; background: var(--pi-surface); color: var(--pi-text); overflow: hidden; }
.session-activity.compacting { border-color: var(--pi-purple-border); background: var(--pi-purple-surface); }
.session-activity.receiving { border-color: var(--pi-success-border); background: var(--pi-success-bg); }
.session-activity strong { color: var(--pi-purple); }
.session-activity.receiving strong { color: var(--pi-success); }
.session-activity span, .session-activity small { color: var(--pi-muted); }
.history-boundary small { color: var(--pi-dim); }
.msg-header { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 8px; }
.msg-header-trailing { min-width: 0; display: inline-flex; align-items: baseline; justify-content: flex-end; gap: 8px; }
.msg-actions { display: inline-flex; gap: 6px; opacity: 0; transition: opacity .12s ease; }
.msg-action { display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #8b949e; padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
.msg-action:hover, .msg-action:focus { color: #e6edf3; border-color: #58a6ff; }
.msg-action { display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
.msg-action:hover, .msg-action:focus { color: var(--pi-text); border-color: var(--pi-accent); }
.msg:hover > .msg-header .msg-actions, .msg:focus-within > .msg-header .msg-actions, .group-msg:hover > .msg-header .msg-actions, .group-msg:focus-within > .msg-header .msg-actions { opacity: 1; }
.label { display: block; color: #8b949e; font-size: 12px; text-transform: uppercase; }
.label { display: block; color: var(--pi-muted); font-size: 12px; text-transform: uppercase; }
.msg-header .label { margin: 0; }
.msg-meta { min-width: 0; opacity: .28; border: 0; background: transparent; color: #6e7681; padding: 0; font: 11px system-ui, sans-serif; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity .12s ease, max-width .12s ease; cursor: pointer; user-select: text; -webkit-user-select: text; }
.msg-meta { min-width: 0; opacity: .28; border: 0; background: transparent; color: var(--pi-dim); padding: 0; font: 11px system-ui, sans-serif; text-align: right; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: opacity .12s ease, max-width .12s ease; cursor: pointer; user-select: text; -webkit-user-select: text; }
.msg:hover > .msg-header .msg-meta, .msg:focus-within > .msg-header .msg-meta, .group-msg:hover > .msg-header .msg-meta, .group-msg:focus-within > .msg-header .msg-meta, .msg-meta:focus, .msg-meta.expanded { opacity: 1; }
.msg-meta:focus { outline: 1px solid #30363d; outline-offset: 3px; border-radius: 4px; }
.msg-meta:focus { outline: 1px solid var(--pi-border); outline-offset: 3px; border-radius: 4px; }
@media (hover: none) {
.msg-actions { opacity: 1; }
.msg-meta { opacity: .75; max-width: 26px; }
@@ -214,16 +216,16 @@ export const chatStyles = css`
formatted-text.part { display: block; }
.part { max-width: 100%; min-width: 0; box-sizing: border-box; overflow: visible; }
.part + .part { margin-top: 10px; }
.tool-line { color: #d29922; }
.summary { color: #8b949e; margin-left: 6px; }
.part:is(details) { border-top: 1px solid #30363d; padding-top: 8px; }
.tool-line { color: var(--pi-warning); }
.summary { color: var(--pi-muted); margin-left: 6px; }
.part:is(details) { border-top: 1px solid var(--pi-border); padding-top: 8px; }
.part > formatted-text { display: block; max-width: 100%; min-width: 0; overflow: visible; }
.skill-invocation, .skill-read { border: 1px solid #30363d; border-radius: 8px; background: #161b22; padding: 8px 10px; }
.skill-invocation > summary, .skill-read > strong { color: #d2a8ff; }
.skill-invocation > small, .skill-read > small { display: block; margin: 6px 0 0; color: #8b949e; }
summary { cursor: pointer; color: #8b949e; }
.skill-invocation, .skill-read { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); padding: 8px 10px; }
.skill-invocation > summary, .skill-read > strong { color: var(--pi-purple); }
.skill-invocation > small, .skill-read > small { display: block; margin: 6px 0 0; color: var(--pi-muted); }
summary { cursor: pointer; color: var(--pi-muted); }
pre { margin: 6px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
.shell-output { color: #e6edf3; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }
.shell-output { color: var(--pi-text); font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.45; }
@keyframes pulse { 0%, 100% { transform: scale(.75); opacity: .55; } 50% { transform: scale(1.2); opacity: 1; } }
`;
@@ -234,15 +236,15 @@ export const formattedTextStyles = css`
:is(p, ul, ol, pre, blockquote, table, .code-block-wrapper):last-child { margin-bottom: 0; }
ul, ol { padding-left: 22px; }
li + li { margin-top: 3px; }
code { border: 1px solid #30363d; border-radius: 4px; background: #0d1117; padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
code { border: 1px solid var(--pi-border); border-radius: 4px; background: var(--pi-bg); padding: 1px 4px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.code-block-wrapper { position: relative; }
.code-block-wrapper pre { margin: 0; padding-right: 40px; }
pre { border: 1px solid #30363d; border-radius: 8px; background: #0d1117; padding: 10px; overflow-x: auto; overflow-y: hidden; }
pre { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); padding: 10px; overflow-x: auto; overflow-y: hidden; }
pre code { border: 0; padding: 0; background: transparent; }
.code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #8b949e; padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
.code-copy-button:hover, .code-copy-button:focus { color: #e6edf3; border-color: #58a6ff; }
blockquote { border-left: 3px solid #30363d; padding-left: 10px; color: #8b949e; }
a { color: #58a6ff; }
.code-copy-button { position: absolute; top: 6px; right: 6px; z-index: 1; display: inline-grid; place-items: center; width: 24px; height: 24px; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 0; font: 14px system-ui, sans-serif; line-height: 1; cursor: pointer; }
.code-copy-button:hover, .code-copy-button:focus { color: var(--pi-text); border-color: var(--pi-accent); }
blockquote { border-left: 3px solid var(--pi-border); padding-left: 10px; color: var(--pi-muted); }
a { color: var(--pi-accent); }
h1, h2, h3, h4 { margin: 14px 0 8px; line-height: 1.2; }
h1:first-child, h2:first-child, h3:first-child, h4:first-child { margin-top: 0; }
h1 { font-size: 20px; }
@@ -250,94 +252,94 @@ export const formattedTextStyles = css`
h3 { font-size: 15px; }
h4 { font-size: 14px; }
table { border-collapse: collapse; display: block; overflow-x: auto; overflow-y: hidden; }
th, td { border: 1px solid #30363d; padding: 4px 8px; }
th { background: #161b22; }
th, td { border: 1px solid var(--pi-border); padding: 4px 8px; }
th { background: var(--pi-surface); }
`;
export const statusBarStyles = css`
:host { display: block; color: #8b949e; font: 12px system-ui, sans-serif; }
.bar { display: flex; gap: 12px; align-items: center; min-width: 0; padding: 7px 12px; border-top: 1px solid #30363d; background: #0d1117; white-space: nowrap; overflow: hidden; }
:host { display: block; color: var(--pi-muted); font: 12px system-ui, sans-serif; }
.bar { display: flex; gap: 12px; align-items: center; min-width: 0; padding: 7px 12px; border-top: 1px solid var(--pi-border); background: var(--pi-bg); white-space: nowrap; overflow: hidden; }
span { overflow: hidden; text-overflow: ellipsis; }
.workspace-label { min-width: 0; display: inline-flex; align-items: baseline; gap: 5px; max-width: 100%; overflow: hidden; white-space: nowrap; }
.workspace-label-base, .workspace-label-item, .workspace-label-render { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: #8b949e; }
.workspace-label-link { color: #58a6ff; text-decoration: none; }
.workspace-label-item, .workspace-label-render, .workspace-label-separator { color: var(--pi-muted); }
.workspace-label-link { color: var(--pi-accent); text-decoration: none; }
.workspace-label-link:hover, .workspace-label-link:focus { text-decoration: underline; }
.bar > span:first-child { flex: 1 1 auto; min-width: 80px; }
.activity { display: inline-flex; align-items: center; gap: 6px; color: #8b949e; }
.activity.active { color: #3fb950; }
.activity { display: inline-flex; align-items: center; gap: 6px; color: var(--pi-muted); }
.activity.active { color: var(--pi-success); }
.dot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; opacity: .45; flex: 0 0 auto; }
.activity.active .dot { animation: pulse 1s ease-in-out infinite; opacity: 1; }
.muted { color: #6e7681; }
.muted { color: var(--pi-dim); }
@keyframes pulse { 0%, 100% { transform: scale(.75); opacity: .55; } 50% { transform: scale(1.2); opacity: 1; } }
`;
export const autocompleteStyles = css`
:host { display: block; }
.menu { position: absolute; left: 0; right: 0; bottom: calc(100% + 6px); z-index: 10; max-height: 260px; overflow: auto; border: 1px solid #30363d; border-radius: 8px; background: #161b22; box-shadow: 0 10px 30px #0008; }
button { display: grid; grid-template-columns: minmax(120px, 1fr) auto; gap: 4px 10px; width: 100%; border: 0; border-bottom: 1px solid #30363d; border-radius: 0; background: transparent; color: #e6edf3; padding: 8px 10px; text-align: left; cursor: pointer; }
.menu { position: absolute; left: 0; right: 0; bottom: calc(100% + 6px); z-index: 10; max-height: 260px; overflow: auto; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); box-shadow: 0 10px 30px var(--pi-shadow); }
button { display: grid; grid-template-columns: minmax(120px, 1fr) auto; gap: 4px 10px; width: 100%; border: 0; border-bottom: 1px solid var(--pi-border); border-radius: 0; background: transparent; color: var(--pi-text); padding: 8px 10px; text-align: left; cursor: pointer; }
button:last-child { border-bottom: 0; }
button.selected, button:hover { background: #0d2847; }
span { color: #8b949e; font-size: 12px; }
small { grid-column: 1 / -1; color: #8b949e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
button.selected, button:hover { background: var(--pi-selection-bg); }
span { color: var(--pi-muted); font-size: 12px; }
small { grid-column: 1 / -1; color: var(--pi-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
`;
export const commandPickerStyles = css`
:host { position: fixed; inset: 0; z-index: 10; color: #e6edf3; font: 14px system-ui, sans-serif; }
.backdrop { display: grid; place-items: center; width: 100%; height: 100%; background: #0008; }
section { width: min(720px, calc(100vw - 40px)); max-height: min(640px, calc(100vh - 40px)); display: flex; flex-direction: column; border: 1px solid #30363d; border-radius: 12px; background: #0d1117; box-shadow: 0 20px 60px #000b; overflow: hidden; }
header { display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid #30363d; }
:host { position: fixed; inset: 0; z-index: 10; color: var(--pi-text); font: 14px system-ui, sans-serif; }
.backdrop { display: grid; place-items: center; width: 100%; height: 100%; background: var(--pi-overlay); }
section { width: min(720px, calc(100vw - 40px)); max-height: min(640px, calc(100vh - 40px)); display: flex; flex-direction: column; border: 1px solid var(--pi-border); border-radius: 12px; background: var(--pi-bg); box-shadow: 0 20px 60px var(--pi-shadow-strong); overflow: hidden; }
header { display: flex; align-items: center; justify-content: space-between; padding: 12px; border-bottom: 1px solid var(--pi-border); }
.options { min-height: 0; overflow: auto; outline: none; }
button { border: 0; background: transparent; color: #e6edf3; cursor: pointer; }
header button { font-size: 20px; color: #8b949e; }
input { margin: 10px 12px; border: 1px solid #30363d; border-radius: 8px; background: #0d1117; color: #e6edf3; font: 14px system-ui, sans-serif; padding: 8px 10px; outline: none; }
input:focus { border-color: #58a6ff; }
.options button { display: block; width: 100%; padding: 10px 12px; border-bottom: 1px solid #21262d; text-align: left; }
.options button.selected, .options button:hover { background: #0d2847; }
small { display: block; margin-top: 4px; color: #8b949e; }
.empty { padding: 24px; color: #8b949e; text-align: center; }
button { border: 0; background: transparent; color: var(--pi-text); cursor: pointer; }
header button { font-size: 20px; color: var(--pi-muted); }
input { margin: 10px 12px; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); color: var(--pi-text); font: 14px system-ui, sans-serif; padding: 8px 10px; outline: none; }
input:focus { border-color: var(--pi-accent); }
.options button { display: block; width: 100%; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted); text-align: left; }
.options button.selected, .options button:hover { background: var(--pi-selection-bg); }
small { display: block; margin-top: 4px; color: var(--pi-muted); }
.empty { padding: 24px; color: var(--pi-muted); text-align: center; }
`;
export const actionPaletteStyles = css`
:host { position: fixed; inset: 0; z-index: 20; color: #e6edf3; font: 14px system-ui, sans-serif; }
.backdrop { --palette-top: min(12dvh, 90px); --palette-bottom: max(20px, env(safe-area-inset-bottom)); display: grid; align-items: start; justify-items: center; width: 100%; height: 100dvh; background: #0008; padding: var(--palette-top) 20px var(--palette-bottom); box-sizing: border-box; overflow: hidden; }
section { width: min(720px, 100%); max-height: min(640px, calc(100dvh - var(--palette-top) - var(--palette-bottom))); display: flex; flex-direction: column; border: 1px solid #30363d; border-radius: 12px; background: #0d1117; box-shadow: 0 20px 60px #000b; overflow: hidden; }
header { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 10px; border-bottom: 1px solid #30363d; }
input { min-width: 0; border: 0; outline: none; background: transparent; color: #e6edf3; font: 16px system-ui, sans-serif; padding: 8px; }
input::placeholder { color: #6e7681; }
button { border: 0; background: transparent; color: #e6edf3; cursor: pointer; }
header button { color: #8b949e; font-size: 22px; padding: 2px 8px; }
:host { position: fixed; inset: 0; z-index: 20; color: var(--pi-text); font: 14px system-ui, sans-serif; }
.backdrop { --palette-top: min(12dvh, 90px); --palette-bottom: max(20px, env(safe-area-inset-bottom)); display: grid; align-items: start; justify-items: center; width: 100%; height: 100dvh; background: var(--pi-overlay); padding: var(--palette-top) 20px var(--palette-bottom); box-sizing: border-box; overflow: hidden; }
section { width: min(720px, 100%); max-height: min(640px, calc(100dvh - var(--palette-top) - var(--palette-bottom))); display: flex; flex-direction: column; border: 1px solid var(--pi-border); border-radius: 12px; background: var(--pi-bg); box-shadow: 0 20px 60px var(--pi-shadow-strong); overflow: hidden; }
header { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 10px; border-bottom: 1px solid var(--pi-border); }
input { min-width: 0; border: 0; outline: none; background: transparent; color: var(--pi-text); font: 16px system-ui, sans-serif; padding: 8px; }
input::placeholder { color: var(--pi-dim); }
button { border: 0; background: transparent; color: var(--pi-text); cursor: pointer; }
header button { color: var(--pi-muted); font-size: 22px; padding: 2px 8px; }
.options { flex: 1 1 auto; min-height: 0; overflow: auto; -webkit-overflow-scrolling: touch; }
.options button { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 12px; width: 100%; padding: 10px 12px; border-bottom: 1px solid #21262d; text-align: left; }
.options button.selected, .options button:hover { background: #0d2847; }
.options button { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 12px; width: 100%; padding: 10px 12px; border-bottom: 1px solid var(--pi-border-muted); text-align: left; }
.options button.selected, .options button:hover { background: var(--pi-selection-bg); }
.main { min-width: 0; }
strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
small { display: block; color: #8b949e; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
small { display: block; color: var(--pi-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.group { grid-column: 1 / -1; font-size: 12px; }
kbd { align-self: center; border: 1px solid #30363d; border-radius: 6px; background: #161b22; color: #8b949e; padding: 2px 6px; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; }
.empty { padding: 24px; color: #8b949e; text-align: center; }
kbd { align-self: center; border: 1px solid var(--pi-border); border-radius: 6px; background: var(--pi-surface); color: var(--pi-muted); padding: 2px 6px; font: 12px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; white-space: nowrap; }
.empty { padding: 24px; color: var(--pi-muted); text-align: center; }
`;
export const promptEditorStyles = css`
:host { position: relative; z-index: 5; display: block; color: #e6edf3; font: 14px system-ui, sans-serif; }
footer { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; padding: 12px; border-top: 1px solid #30363d; }
footer.shell-mode { border-top-color: #3fb950; background: #0f1b12; }
:host { position: relative; z-index: 5; display: block; color: var(--pi-text); font: 14px system-ui, sans-serif; }
footer { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; padding: 12px; border-top: 1px solid var(--pi-border); }
footer.shell-mode { border-top-color: var(--pi-success); background: var(--pi-success-bg); }
.editor-wrap { position: relative; min-width: 0; }
.actions { display: flex; gap: 8px; align-items: center; justify-content: flex-end; flex-wrap: nowrap; white-space: nowrap; }
.compact-status { display: flex; min-width: 0; align-items: center; gap: 6px; color: #8b949e; font-size: 12px; flex: 1 1 0; }
.compact-status { display: flex; min-width: 0; align-items: center; gap: 6px; color: var(--pi-muted); font-size: 12px; flex: 1 1 0; }
.compact-status > button { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.select-model { max-width: min(42vw, 320px); }
.select-thinking { max-width: 110px; }
textarea, .markdown-editor .cm-editor { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow: hidden; border-radius: 8px; border: 1px solid #30363d; background: #0d1117; color: #e6edf3; font: 16px/1.4 system-ui, sans-serif; }
textarea, .markdown-editor .cm-editor { box-sizing: border-box; width: 100%; min-height: 54px; max-height: 220px; resize: none; overflow: hidden; border-radius: 8px; border: 1px solid var(--pi-border); background: var(--pi-bg); color: var(--pi-text); font: 16px/1.4 system-ui, sans-serif; }
textarea { overflow-y: auto; padding: 8px; }
.markdown-editor .cm-scroller { max-height: 220px; overflow-y: auto; font-family: system-ui, sans-serif; line-height: 1.4; }
.markdown-editor .cm-content { min-height: 38px; padding: 8px; caret-color: #e6edf3; }
.markdown-editor .cm-content { min-height: 38px; padding: 8px; caret-color: var(--pi-text); }
.markdown-editor .cm-line { padding: 0; }
.markdown-editor .cm-placeholder { color: #6e7681; }
.markdown-editor .cm-placeholder { color: var(--pi-dim); }
.markdown-editor .cm-focused { outline: none; }
.shell-mode textarea, .shell-mode .markdown-editor .cm-editor { border-color: #3fb950; box-shadow: 0 0 0 1px #3fb95055; }
.mode-hint { position: absolute; right: 8px; bottom: 8px; max-width: calc(100% - 16px); border: 1px solid #238636; border-radius: 999px; background: #0f2a16; color: #3fb950; padding: 2px 8px; font-size: 12px; pointer-events: none; }
button { border: 1px solid #30363d; border-radius: 8px; background: #161b22; color: #e6edf3; padding: 7px 9px; cursor: pointer; }
.shell-mode textarea, .shell-mode .markdown-editor .cm-editor { border-color: var(--pi-success); box-shadow: 0 0 0 1px var(--pi-success-ring); }
.mode-hint { position: absolute; right: 8px; bottom: 8px; max-width: calc(100% - 16px); border: 1px solid var(--pi-success-border); border-radius: 999px; background: var(--pi-success-surface); color: var(--pi-success); padding: 2px 8px; font-size: 12px; pointer-events: none; }
button { border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-surface); color: var(--pi-text); padding: 7px 9px; cursor: pointer; }
button:disabled, textarea:disabled, .markdown-editor-disabled .cm-editor { opacity: .5; cursor: not-allowed; }
@media (max-width: 640px) {
footer { gap: 8px; padding: 8px; }
+7
View File
@@ -39,6 +39,13 @@ export function createCoreActions(): PluginAction[] {
group: "General",
run: (context) => context.logoutAuth(),
},
{
id: "theme.select",
title: "Select Theme",
description: "Choose the Pi Web color theme",
group: "Preferences",
run: (context) => { context.openThemePicker(); },
},
{
id: "view.chat",
title: "Go to Chat",
+78 -1
View File
@@ -3,7 +3,8 @@ import type { Workspace } from "../api";
import { initialAppState, type AppState } from "../appState";
import { corePlugin } from "./core";
import { PluginRegistry } from "./registry";
import type { PluginRuntimeContext } from "./types";
import { themePackPlugin } from "./themes";
import type { PluginRuntimeContext, ThemeTokens } from "./types";
function createContext(statePatch: Partial<AppState> = {}) {
const calls: string[] = [];
@@ -14,6 +15,7 @@ function createContext(statePatch: Partial<AppState> = {}) {
addProject: vi.fn(() => { calls.push("addProject"); }),
configureAuth: vi.fn(() => { calls.push("configureAuth"); }),
logoutAuth: vi.fn(() => { calls.push("logoutAuth"); }),
openThemePicker: vi.fn(() => { calls.push("openThemePicker"); }),
selectMainView: vi.fn((view: AppState["mainView"]) => { calls.push(`selectMainView:${view}`); }),
selectWorkspaceTool: vi.fn((tool: AppState["workspaceTool"]) => { calls.push(`selectWorkspaceTool:${tool}`); }),
refreshFiles: vi.fn(() => { calls.push("refreshFiles"); }),
@@ -81,6 +83,41 @@ describe("PluginRegistry", () => {
expect(calls).toEqual(["refreshGit"]);
});
it("collects built-in Pi Web themes from an in-app plugin", () => {
const registry = new PluginRegistry();
registry.register({ id: "themes", plugin: themePackPlugin });
expect(registry.getThemes().map((theme) => ({ id: theme.id, colorScheme: theme.colorScheme }))).toEqual([
{ id: "themes:current", colorScheme: "dark" },
{ id: "themes:docs-dark", colorScheme: "dark" },
{ id: "themes:docs-light", colorScheme: "light" },
]);
});
it("collects theme contributions in contribution order", () => {
const registry = new PluginRegistry();
registry.register({
id: "example",
plugin: {
apiVersion: 1,
name: "Example",
activate: () => ({
contributions: {
themes: [
{ id: "last", name: "Last", order: 20, colorScheme: "dark", tokens: testThemeTokens() },
{ id: "first", name: "First", order: 10, colorScheme: "light", tokens: testThemeTokens() },
],
},
}),
},
});
expect(registry.getThemes().map((theme) => ({ id: theme.id, pluginId: theme.pluginId, localId: theme.localId, name: theme.name }))).toEqual([
{ id: "example:first", pluginId: "example", localId: "first", name: "First" },
{ id: "example:last", pluginId: "example", localId: "last", name: "Last" },
]);
});
it("collects workspace label items in contribution order", () => {
const registry = new PluginRegistry();
const workspace = testWorkspace();
@@ -111,3 +148,43 @@ describe("PluginRegistry", () => {
function testWorkspace(): Workspace {
return { id: "w1", projectId: "p1", path: "/tmp/project", label: "main", isMain: true, isGitRepo: true, isGitWorktree: false };
}
function testThemeTokens(): ThemeTokens {
return {
"--pi-bg": "#000000",
"--pi-surface": "#000000",
"--pi-surface-hover": "#000000",
"--pi-terminal-bg": "#000000",
"--pi-terminal-text": "#000000",
"--pi-border": "#000000",
"--pi-border-muted": "#000000",
"--pi-text": "#000000",
"--pi-text-secondary": "#000000",
"--pi-text-bright": "#000000",
"--pi-muted": "#000000",
"--pi-dim": "#000000",
"--pi-accent": "#000000",
"--pi-accent-border": "#000000",
"--pi-selection-bg": "#000000",
"--pi-success": "#000000",
"--pi-success-border": "#000000",
"--pi-success-bg": "#000000",
"--pi-success-surface": "#000000",
"--pi-success-ring": "#000000",
"--pi-warning": "#000000",
"--pi-warning-border": "#000000",
"--pi-warning-surface": "#000000",
"--pi-danger": "#000000",
"--pi-purple": "#000000",
"--pi-purple-border": "#000000",
"--pi-purple-surface": "#000000",
"--pi-overlay": "#000000",
"--pi-shadow-soft": "#000000",
"--pi-shadow": "#000000",
"--pi-shadow-strong": "#000000",
"--pi-bg-overlay-soft": "#000000",
"--pi-bg-overlay": "#000000",
"--pi-success-bg-overlay": "#000000",
"--pi-terminal-selection": "#000000",
};
}
+12 -1
View File
@@ -1,7 +1,7 @@
import { html } from "lit";
import type { AppState } from "../appState";
import type { Workspace } from "../api";
import type { PiWebPluginRegistration, PluginAction, PluginRuntimeContext, QualifiedContributionId, QualifiedPluginAction, QualifiedWorkspaceLabelContribution, QualifiedWorkspacePanelContribution, WorkspaceLabelContribution, WorkspaceLabelItem, WorkspacePanelContribution } from "./types";
import type { PiWebPluginRegistration, PluginAction, PluginRuntimeContext, QualifiedContributionId, QualifiedPluginAction, QualifiedThemeContribution, QualifiedWorkspaceLabelContribution, QualifiedWorkspacePanelContribution, ThemeContribution, WorkspaceLabelContribution, WorkspaceLabelItem, WorkspacePanelContribution } from "./types";
const idPattern = /^[a-z][a-z0-9.-]*$/u;
const localIdPattern = /^[a-z][a-z0-9.-]*$/u;
@@ -16,6 +16,7 @@ export class PluginRegistry {
private readonly actions: RegisteredPluginAction[] = [];
private readonly workspacePanels: QualifiedWorkspacePanelContribution[] = [];
private readonly workspaceLabels: QualifiedWorkspaceLabelContribution[] = [];
private readonly themes: QualifiedThemeContribution[] = [];
private readonly pluginIds = new Set<string>();
private readonly contributionIds = new Set<QualifiedContributionId>();
@@ -32,6 +33,7 @@ export class PluginRegistry {
for (const action of contributions.actions ?? []) this.actions.push(this.qualifyAction(id, action));
for (const panel of contributions.workspacePanels ?? []) this.workspacePanels.push(this.qualifyWorkspacePanel(id, panel));
for (const contribution of contributions.workspaceLabels ?? []) this.workspaceLabels.push(this.qualifyWorkspaceLabelContribution(id, contribution));
for (const theme of contributions.themes ?? []) this.themes.push(this.qualifyTheme(id, theme));
}
getActions(context: PluginRuntimeContext): QualifiedPluginAction[] {
@@ -56,6 +58,10 @@ export class PluginRegistry {
return [...this.workspacePanels].sort((left, right) => (left.order ?? 1000) - (right.order ?? 1000) || left.title.localeCompare(right.title));
}
getThemes(): QualifiedThemeContribution[] {
return [...this.themes].sort((left, right) => (left.order ?? 1000) - (right.order ?? 1000) || left.name.localeCompare(right.name));
}
getWorkspaceLabelItems(state: AppState, workspace: Workspace): WorkspaceLabelItem[] {
const context = { state, workspace };
return [...this.workspaceLabels]
@@ -81,6 +87,11 @@ export class PluginRegistry {
return { ...contribution, id, pluginId, localId: contribution.id };
}
private qualifyTheme(pluginId: string, theme: ThemeContribution): QualifiedThemeContribution {
const id = this.qualify(pluginId, theme.id);
return { ...theme, id, pluginId, localId: theme.id };
}
private qualify(pluginId: string, localId: string): QualifiedContributionId {
this.validateLocalId(localId);
const qualified: QualifiedContributionId = `${pluginId}:${localId}`;
+150
View File
@@ -0,0 +1,150 @@
import type { PiWebPlugin, ThemeTokens } from "../types";
const currentTokens = {
"--pi-bg": "#0d1117",
"--pi-surface": "#161b22",
"--pi-surface-hover": "#21262d",
"--pi-terminal-bg": "#05070a",
"--pi-terminal-text": "#e6edf3",
"--pi-border": "#30363d",
"--pi-border-muted": "#21262d",
"--pi-text": "#e6edf3",
"--pi-text-secondary": "#c9d1d9",
"--pi-text-bright": "#f0f6fc",
"--pi-muted": "#8b949e",
"--pi-dim": "#6e7681",
"--pi-accent": "#58a6ff",
"--pi-accent-border": "#2f81f7",
"--pi-selection-bg": "#0d2847",
"--pi-success": "#3fb950",
"--pi-success-border": "#238636",
"--pi-success-bg": "#0f1b12",
"--pi-success-surface": "#0f2a16",
"--pi-success-ring": "#3fb95055",
"--pi-warning": "#d29922",
"--pi-warning-border": "#6e5200",
"--pi-warning-surface": "#1f1a10",
"--pi-danger": "#ff7b72",
"--pi-purple": "#d2a8ff",
"--pi-purple-border": "#a371f7",
"--pi-purple-surface": "#21132f",
"--pi-overlay": "#0008",
"--pi-shadow-soft": "#0006",
"--pi-shadow": "#0008",
"--pi-shadow-strong": "#000b",
"--pi-bg-overlay-soft": "#0d1117dd",
"--pi-bg-overlay": "#0d1117e6",
"--pi-success-bg-overlay": "#0f1b12ee",
"--pi-terminal-selection": "#264f78",
} satisfies ThemeTokens;
const docsDarkTokens = {
"--pi-bg": "#070912",
"--pi-surface": "#101527",
"--pi-surface-hover": "#151b31",
"--pi-terminal-bg": "#050710",
"--pi-terminal-text": "#f7f4ff",
"--pi-border": "#26304f",
"--pi-border-muted": "#26304f",
"--pi-text": "#f7f4ff",
"--pi-text-secondary": "#aaa4bd",
"--pi-text-bright": "#ffffff",
"--pi-muted": "#aaa4bd",
"--pi-dim": "#817a99",
"--pi-accent": "#7c3cff",
"--pi-accent-border": "#3d4a78",
"--pi-selection-bg": "#151b31",
"--pi-success": "#00f0d8",
"--pi-success-border": "#00f0d8",
"--pi-success-bg": "#071e22",
"--pi-success-surface": "#092d31",
"--pi-success-ring": "#00f0d855",
"--pi-warning": "#ffb000",
"--pi-warning-border": "#ffb000",
"--pi-warning-surface": "#16140d",
"--pi-danger": "#ff4f7b",
"--pi-purple": "#b7a2ff",
"--pi-purple-border": "#7c3cff",
"--pi-purple-surface": "#181026",
"--pi-overlay": "#0008",
"--pi-shadow-soft": "#0006",
"--pi-shadow": "#0008",
"--pi-shadow-strong": "#000b",
"--pi-bg-overlay-soft": "#070912dd",
"--pi-bg-overlay": "#070912e6",
"--pi-success-bg-overlay": "#071e22ee",
"--pi-terminal-selection": "#3d4a78",
} satisfies ThemeTokens;
const docsLightTokens = {
"--pi-bg": "#f7f1e6",
"--pi-surface": "#fff9ee",
"--pi-surface-hover": "#f0e6d6",
"--pi-terminal-bg": "#15131b",
"--pi-terminal-text": "#fff9ee",
"--pi-border": "#c9bca8",
"--pi-border-muted": "#d8cdbc",
"--pi-text": "#15131b",
"--pi-text-secondary": "#15131b",
"--pi-text-bright": "#15131b",
"--pi-muted": "#5f586a",
"--pi-dim": "#766f7e",
"--pi-accent": "#6430d8",
"--pi-accent-border": "#6430d8",
"--pi-selection-bg": "#eadff8",
"--pi-success": "#008c82",
"--pi-success-border": "#008c82",
"--pi-success-bg": "#e1f4ef",
"--pi-success-surface": "#d7f0ec",
"--pi-success-ring": "#008c8255",
"--pi-warning": "#b05f00",
"--pi-warning-border": "#b05f00",
"--pi-warning-surface": "#fff2d2",
"--pi-danger": "#b51d49",
"--pi-purple": "#6430d8",
"--pi-purple-border": "#6430d8",
"--pi-purple-surface": "#eadff8",
"--pi-overlay": "#15131b66",
"--pi-shadow-soft": "#15131b22",
"--pi-shadow": "#15131b33",
"--pi-shadow-strong": "#15131b44",
"--pi-bg-overlay-soft": "#f7f1e6dd",
"--pi-bg-overlay": "#f7f1e6e6",
"--pi-success-bg-overlay": "#e1f4efee",
"--pi-terminal-selection": "#8d7b64",
} satisfies ThemeTokens;
export const themePackPlugin: PiWebPlugin = {
apiVersion: 1,
name: "Pi Web Themes",
activate: () => ({
contributions: {
themes: [
{
id: "current",
name: "Pi Web Current",
description: "The original Pi Web dark palette.",
order: 10,
colorScheme: "dark",
tokens: currentTokens,
},
{
id: "docs-dark",
name: "Docs Dark",
description: "Dark theme based on the Pi Web docs site.",
order: 20,
colorScheme: "dark",
tokens: docsDarkTokens,
},
{
id: "docs-light",
name: "Docs Light",
description: "Light theme based on the Pi Web docs site.",
order: 30,
colorScheme: "light",
tokens: docsLightTokens,
},
],
},
}),
};
+58
View File
@@ -33,6 +33,7 @@ export interface PluginContributions {
actions?: PluginAction[];
workspacePanels?: WorkspacePanelContribution[];
workspaceLabels?: WorkspaceLabelContribution[];
themes?: ThemeContribution[];
}
export interface PluginRuntimeContext {
@@ -42,6 +43,7 @@ export interface PluginRuntimeContext {
addProject: () => void | Promise<void>;
configureAuth: () => void | Promise<void>;
logoutAuth: () => void | Promise<void>;
openThemePicker: () => void;
selectMainView: (view: AppState["mainView"]) => void;
selectWorkspaceTool: (tool: QualifiedContributionId) => void;
refreshFiles: () => void | Promise<void>;
@@ -140,6 +142,62 @@ export interface WorkspaceLabelContribution {
items: (context: WorkspaceLabelContext) => WorkspaceLabelItem[];
}
export type ThemeColorScheme = "dark" | "light";
export type ThemeToken =
| "--pi-bg"
| "--pi-surface"
| "--pi-surface-hover"
| "--pi-terminal-bg"
| "--pi-terminal-text"
| "--pi-border"
| "--pi-border-muted"
| "--pi-text"
| "--pi-text-secondary"
| "--pi-text-bright"
| "--pi-muted"
| "--pi-dim"
| "--pi-accent"
| "--pi-accent-border"
| "--pi-selection-bg"
| "--pi-success"
| "--pi-success-border"
| "--pi-success-bg"
| "--pi-success-surface"
| "--pi-success-ring"
| "--pi-warning"
| "--pi-warning-border"
| "--pi-warning-surface"
| "--pi-danger"
| "--pi-purple"
| "--pi-purple-border"
| "--pi-purple-surface"
| "--pi-overlay"
| "--pi-shadow-soft"
| "--pi-shadow"
| "--pi-shadow-strong"
| "--pi-bg-overlay-soft"
| "--pi-bg-overlay"
| "--pi-success-bg-overlay"
| "--pi-terminal-selection";
export type ThemeTokens = Record<ThemeToken, string>;
export interface ThemeContribution {
id: LocalContributionId;
name: string;
description?: string;
order?: number;
colorScheme: ThemeColorScheme;
tokens: ThemeTokens;
}
export interface QualifiedThemeContribution extends ThemeContribution {
id: QualifiedContributionId;
pluginId: PluginId;
localId: LocalContributionId;
}
export interface QualifiedWorkspaceLabelContribution extends WorkspaceLabelContribution {
id: QualifiedContributionId;
pluginId: PluginId;
+74
View File
@@ -0,0 +1,74 @@
import type { QualifiedContributionId, QualifiedThemeContribution, ThemeToken } from "./plugins/types";
export const DEFAULT_THEME_ID: QualifiedContributionId = "themes:current";
export const THEME_STORAGE_KEY = "pi-web-app-theme";
export const THEME_TOKENS: ThemeToken[] = [
"--pi-bg",
"--pi-surface",
"--pi-surface-hover",
"--pi-terminal-bg",
"--pi-terminal-text",
"--pi-border",
"--pi-border-muted",
"--pi-text",
"--pi-text-secondary",
"--pi-text-bright",
"--pi-muted",
"--pi-dim",
"--pi-accent",
"--pi-accent-border",
"--pi-selection-bg",
"--pi-success",
"--pi-success-border",
"--pi-success-bg",
"--pi-success-surface",
"--pi-success-ring",
"--pi-warning",
"--pi-warning-border",
"--pi-warning-surface",
"--pi-danger",
"--pi-purple",
"--pi-purple-border",
"--pi-purple-surface",
"--pi-overlay",
"--pi-shadow-soft",
"--pi-shadow",
"--pi-shadow-strong",
"--pi-bg-overlay-soft",
"--pi-bg-overlay",
"--pi-success-bg-overlay",
"--pi-terminal-selection",
];
const qualifiedContributionIdPattern = /^[a-z][a-z0-9.-]*:[a-z][a-z0-9.-]*$/u;
export function readStoredThemeId(): QualifiedContributionId | undefined {
try {
const value = window.localStorage.getItem(THEME_STORAGE_KEY);
return isQualifiedContributionId(value) ? value : undefined;
} catch {
return undefined;
}
}
export function applyPiWebTheme(theme: QualifiedThemeContribution, options: { persist?: boolean } = {}): void {
const root = document.documentElement;
root.dataset["piWebTheme"] = theme.id;
root.style.colorScheme = theme.colorScheme;
for (const token of THEME_TOKENS) {
const value = theme.tokens[token];
if (typeof value === "string" && value !== "") root.style.setProperty(token, value);
else root.style.removeProperty(token);
}
if (options.persist === false) return;
try {
window.localStorage.setItem(THEME_STORAGE_KEY, theme.id);
} catch {
// Ignore storage failures; the selected theme can still apply for this tab.
}
}
function isQualifiedContributionId(value: string | null): value is QualifiedContributionId {
return value !== null && qualifiedContributionIdPattern.test(value);
}