diff --git a/.changeset/themable-current-colors.md b/.changeset/themable-current-colors.md
new file mode 100644
index 0000000..436e6ff
--- /dev/null
+++ b/.changeset/themable-current-colors.md
@@ -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.
diff --git a/src/client/index.html b/src/client/index.html
index 572ac6e..12bc470 100644
--- a/src/client/index.html
+++ b/src/client/index.html
@@ -5,7 +5,45 @@
Pi Web
diff --git a/src/client/src/appState.ts b/src/client/src/appState.ts
index 5ef6f89..c5fcfd2 100644
--- a/src/client/src/appState.ts
+++ b/src/client/src/appState.ts
@@ -21,6 +21,7 @@ export interface AppState {
commandDialog: Extract | 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,
diff --git a/src/client/src/components/AuthDialog.ts b/src/client/src/components/AuthDialog.ts
index 5c5b5c1..28a5698 100644
--- a/src/client/src/components/AuthDialog.ts
+++ b/src/client/src/components/AuthDialog.ts
@@ -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; }
`];
}
diff --git a/src/client/src/components/CodeViewer.ts b/src/client/src/components/CodeViewer.ts
index 57ebcee..7523f15 100644
--- a/src/client/src/components/CodeViewer.ts
+++ b/src/client/src/components/CodeViewer.ts
@@ -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",
diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts
index a06f47c..35ee626 100644
--- a/src/client/src/components/PiWebApp.ts
+++ b/src/client/src/components/PiWebApp.ts
@@ -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 {
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` { this.setState({ actionPaletteOpen: false }); this.runAction(actionId); }} .onCancel=${() => { this.setState({ actionPaletteOpen: false }); }}>` : null}
${state.projectDialogOpen ? html` this.projects.addProject(path, create)} .onCancel=${() => { this.setState({ projectDialogOpen: false }); }}>` : null}
+ ${state.themeDialog !== undefined ? html` { this.pickTheme(value); }} .onCancel=${() => { this.setState({ themeDialog: undefined }); }}>` : null}
`;
}
@@ -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;
}
diff --git a/src/client/src/components/ProjectDialog.ts b/src/client/src/components/ProjectDialog.ts
index 8a034ae..b89a82d 100644
--- a/src/client/src/components/ProjectDialog.ts
+++ b/src/client/src/components/ProjectDialog.ts
@@ -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; }
`;
}
diff --git a/src/client/src/components/TerminalPanel.ts b/src/client/src/components/TerminalPanel.ts
index f4ea184..0e2cd91 100644
--- a/src/client/src/components/TerminalPanel.ts
+++ b/src/client/src/components/TerminalPanel.ts
@@ -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 {
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) };
diff --git a/src/client/src/components/shared.ts b/src/client/src/components/shared.ts
index 9fb4b36..038a2cc 100644
--- a/src/client/src/components/shared.ts
+++ b/src/client/src/components/shared.ts
@@ -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; }
diff --git a/src/client/src/plugins/core/actions.ts b/src/client/src/plugins/core/actions.ts
index 9af93ca..c7fb7e6 100644
--- a/src/client/src/plugins/core/actions.ts
+++ b/src/client/src/plugins/core/actions.ts
@@ -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",
diff --git a/src/client/src/plugins/registry.test.ts b/src/client/src/plugins/registry.test.ts
index 57ceff6..2ed1544 100644
--- a/src/client/src/plugins/registry.test.ts
+++ b/src/client/src/plugins/registry.test.ts
@@ -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 = {}) {
const calls: string[] = [];
@@ -14,6 +15,7 @@ function createContext(statePatch: Partial = {}) {
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",
+ };
+}
diff --git a/src/client/src/plugins/registry.ts b/src/client/src/plugins/registry.ts
index 6736454..26d821d 100644
--- a/src/client/src/plugins/registry.ts
+++ b/src/client/src/plugins/registry.ts
@@ -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();
private readonly contributionIds = new Set();
@@ -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}`;
diff --git a/src/client/src/plugins/themes/index.ts b/src/client/src/plugins/themes/index.ts
new file mode 100644
index 0000000..9e03cdb
--- /dev/null
+++ b/src/client/src/plugins/themes/index.ts
@@ -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,
+ },
+ ],
+ },
+ }),
+};
diff --git a/src/client/src/plugins/types.ts b/src/client/src/plugins/types.ts
index a583fd8..b0dc1b4 100644
--- a/src/client/src/plugins/types.ts
+++ b/src/client/src/plugins/types.ts
@@ -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;
configureAuth: () => void | Promise;
logoutAuth: () => void | Promise;
+ openThemePicker: () => void;
selectMainView: (view: AppState["mainView"]) => void;
selectWorkspaceTool: (tool: QualifiedContributionId) => void;
refreshFiles: () => void | Promise;
@@ -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;
+
+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;
diff --git a/src/client/src/theme.ts b/src/client/src/theme.ts
new file mode 100644
index 0000000..f3a3fa9
--- /dev/null
+++ b/src/client/src/theme.ts
@@ -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);
+}