mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
feat(coding-agent): add scrollbar thumb theme color
This commit is contained in:
parent
7724472eab
commit
b3ed27b3fe
8 changed files with 81 additions and 10 deletions
|
|
@ -9,6 +9,7 @@
|
|||
- Added a sticky editor, status, widget, and footer dock to fullscreen mode while keeping the transcript independently scrollable.
|
||||
- Added a draggable transcript scrollbar to fullscreen mode with configurable `auto`, `always`, and `hidden` modes through `/settings`; `always` reserves the rightmost column.
|
||||
- Added page scrolling and marked-message navigation shortcuts to fullscreen mode.
|
||||
- Added an optional `scrollbarThumb` theme color for fullscreen scrollbar thumbs, falling back to `selectedBg`.
|
||||
|
||||
### Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,7 @@ vim ~/.pi/agent/themes/my-theme.json
|
|||
"text": "",
|
||||
"thinkingText": "secondary",
|
||||
"selectedBg": "#2d2d30",
|
||||
"scrollbarThumb": "#555566",
|
||||
"userMessageBg": "#2d2d30",
|
||||
"userMessageText": "",
|
||||
"customMessageBg": "#2d2d30",
|
||||
|
|
@ -140,13 +141,13 @@ vim ~/.pi/agent/themes/my-theme.json
|
|||
|
||||
- `name` is required, must be unique, and must not contain `/`.
|
||||
- `vars` is optional. Define reusable colors here, then reference them in `colors`.
|
||||
- `colors` must define all 51 required tokens. `thinkingMax` is optional and falls back to `thinkingXhigh`.
|
||||
- `colors` must define all 51 required tokens. `thinkingMax` is optional and falls back to `thinkingXhigh`; `scrollbarThumb` is optional and falls back to `selectedBg`.
|
||||
|
||||
The `$schema` field enables editor auto-completion and validation.
|
||||
|
||||
## Color Tokens
|
||||
|
||||
Every theme must define all 51 required color tokens. `thinkingMax` is optional for compatibility with existing themes; when omitted, it uses `thinkingXhigh`.
|
||||
Every theme must define all 51 required color tokens. `thinkingMax` and `scrollbarThumb` are optional for compatibility with existing themes; when omitted, they use `thinkingXhigh` and `selectedBg`, respectively.
|
||||
|
||||
### Core UI (11 colors)
|
||||
|
||||
|
|
@ -164,11 +165,12 @@ Every theme must define all 51 required color tokens. `thinkingMax` is optional
|
|||
| `text` | Default text (usually `""`) |
|
||||
| `thinkingText` | Thinking block text |
|
||||
|
||||
### Backgrounds & Content (11 colors)
|
||||
### Backgrounds & Content (11 required, 1 optional)
|
||||
|
||||
| Token | Purpose |
|
||||
|-------|---------|
|
||||
| `selectedBg` | Selected line background |
|
||||
| `scrollbarThumb` | Fullscreen scrollbar thumb background; optional, falls back to `selectedBg` |
|
||||
| `userMessageBg` | User message background |
|
||||
| `userMessageText` | User message text |
|
||||
| `customMessageBg` | Extension message background |
|
||||
|
|
|
|||
|
|
@ -751,7 +751,7 @@ export class InteractiveMode {
|
|||
primary: true,
|
||||
overscroll: "chain",
|
||||
scrollbar: this.settingsManager.getFullscreenScrollbar(),
|
||||
scrollbarStyle: (text) => theme.bg("selectedBg", text),
|
||||
scrollbarStyle: (text) => theme.bg("scrollbarThumb", text),
|
||||
});
|
||||
const dock = new TuiLayouts.VStack([
|
||||
{ component: this.pendingMessagesContainer, shrink: 1, minSize: 0 },
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
"thinkingText": "gray",
|
||||
|
||||
"selectedBg": "selectedBg",
|
||||
"scrollbarThumb": "selectedBg",
|
||||
"userMessageBg": "userMsgBg",
|
||||
"userMessageText": "text",
|
||||
"customMessageBg": "customMsgBg",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"thinkingText": "mediumGray",
|
||||
|
||||
"selectedBg": "selectedBg",
|
||||
"scrollbarThumb": "selectedBg",
|
||||
"userMessageBg": "userMsgBg",
|
||||
"userMessageText": "text",
|
||||
"customMessageBg": "customMsgBg",
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@
|
|||
},
|
||||
"colors": {
|
||||
"type": "object",
|
||||
"description": "Theme color definitions (thinkingMax is optional and falls back to thinkingXhigh)",
|
||||
"description": "Theme color definitions (thinkingMax and scrollbarThumb are optional and use compatible fallbacks)",
|
||||
"required": [
|
||||
"accent",
|
||||
"border",
|
||||
|
|
@ -137,6 +137,10 @@
|
|||
"$ref": "#/$defs/colorValue",
|
||||
"description": "Selected item background"
|
||||
},
|
||||
"scrollbarThumb": {
|
||||
"$ref": "#/$defs/colorValue",
|
||||
"description": "Fullscreen scrollbar thumb background (falls back to selectedBg when omitted)"
|
||||
},
|
||||
"userMessageBg": {
|
||||
"$ref": "#/$defs/colorValue",
|
||||
"description": "User message background"
|
||||
|
|
|
|||
|
|
@ -45,8 +45,9 @@ const ThemeJsonSchema = Type.Object({
|
|||
dim: ColorValueSchema,
|
||||
text: ColorValueSchema,
|
||||
thinkingText: ColorValueSchema,
|
||||
// Backgrounds & Content Text (11 colors)
|
||||
// Backgrounds & Content Text (11 required, 1 optional)
|
||||
selectedBg: ColorValueSchema,
|
||||
scrollbarThumb: Type.Optional(ColorValueSchema),
|
||||
userMessageBg: ColorValueSchema,
|
||||
userMessageText: ColorValueSchema,
|
||||
customMessageBg: ColorValueSchema,
|
||||
|
|
@ -156,6 +157,7 @@ export type ThemeColor =
|
|||
|
||||
export type ThemeBg =
|
||||
| "selectedBg"
|
||||
| "scrollbarThumb"
|
||||
| "userMessageBg"
|
||||
| "customMessageBg"
|
||||
| "toolPendingBg"
|
||||
|
|
@ -319,8 +321,14 @@ function resolveThemeColors<T extends Record<string, ColorValue>>(
|
|||
return resolved as Record<keyof T, string | number>;
|
||||
}
|
||||
|
||||
function withThemeColorFallbacks(colors: ThemeJson["colors"]): ThemeJson["colors"] & { thinkingMax: ColorValue } {
|
||||
return { ...colors, thinkingMax: colors.thinkingMax ?? colors.thinkingXhigh };
|
||||
function withThemeColorFallbacks(
|
||||
colors: ThemeJson["colors"],
|
||||
): ThemeJson["colors"] & { thinkingMax: ColorValue; scrollbarThumb: ColorValue } {
|
||||
return {
|
||||
...colors,
|
||||
thinkingMax: colors.thinkingMax ?? colors.thinkingXhigh,
|
||||
scrollbarThumb: colors.scrollbarThumb ?? colors.selectedBg,
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -337,7 +345,8 @@ export class Theme {
|
|||
|
||||
constructor(
|
||||
fgColors: Record<ThemeColor, string | number>,
|
||||
bgColors: Record<ThemeBg, string | number>,
|
||||
bgColors: Record<Exclude<ThemeBg, "scrollbarThumb">, string | number> &
|
||||
Partial<Record<"scrollbarThumb", string | number>>,
|
||||
mode: ColorMode,
|
||||
options: { name?: string; sourcePath?: string; sourceInfo?: SourceInfo } = {},
|
||||
) {
|
||||
|
|
@ -351,7 +360,11 @@ export class Theme {
|
|||
this.fgColors.set(key, fgAnsi(value, mode));
|
||||
}
|
||||
this.bgColors = new Map();
|
||||
for (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {
|
||||
const backgrounds = {
|
||||
...bgColors,
|
||||
scrollbarThumb: bgColors.scrollbarThumb ?? bgColors.selectedBg,
|
||||
};
|
||||
for (const [key, value] of Object.entries(backgrounds) as [ThemeBg, string | number][]) {
|
||||
this.bgColors.set(key, bgAnsi(value, mode));
|
||||
}
|
||||
}
|
||||
|
|
@ -601,6 +614,7 @@ function createTheme(themeJson: ThemeJson, mode?: ColorMode, sourcePath?: string
|
|||
const bgColors: Record<ThemeBg, string | number> = {} as Record<ThemeBg, string | number>;
|
||||
const bgColorKeys: Set<string> = new Set([
|
||||
"selectedBg",
|
||||
"scrollbarThumb",
|
||||
"userMessageBg",
|
||||
"customMessageBg",
|
||||
"toolPendingBg",
|
||||
|
|
|
|||
48
packages/coding-agent/test/scrollbar-theme.test.ts
Normal file
48
packages/coding-agent/test/scrollbar-theme.test.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { loadThemeFromPath } from "../src/modes/interactive/theme/theme.ts";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function loadDarkTheme(): { name: string; colors: Record<string, string | number> } {
|
||||
return JSON.parse(readFileSync(new URL("../src/modes/interactive/theme/dark.json", import.meta.url), "utf8")) as {
|
||||
name: string;
|
||||
colors: Record<string, string | number>;
|
||||
};
|
||||
}
|
||||
|
||||
function writeTheme(theme: { name: string; colors: Record<string, string | number> }): string {
|
||||
const testDir = mkdtempSync(join(tmpdir(), "pi-scrollbar-theme-"));
|
||||
tempDirs.push(testDir);
|
||||
const themePath = join(testDir, `${theme.name}.json`);
|
||||
writeFileSync(themePath, JSON.stringify(theme));
|
||||
return themePath;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
describe("scrollbar theme color", () => {
|
||||
it("falls back to selectedBg when scrollbarThumb is omitted", () => {
|
||||
const themeJson = loadDarkTheme();
|
||||
themeJson.name = "legacy-scrollbar-theme";
|
||||
delete themeJson.colors.scrollbarThumb;
|
||||
|
||||
const loadedTheme = loadThemeFromPath(writeTheme(themeJson), "truecolor");
|
||||
expect(loadedTheme.getBgAnsi("scrollbarThumb")).toBe(loadedTheme.getBgAnsi("selectedBg"));
|
||||
});
|
||||
|
||||
it("uses an explicitly configured scrollbarThumb", () => {
|
||||
const themeJson = loadDarkTheme();
|
||||
themeJson.name = "custom-scrollbar-theme";
|
||||
themeJson.colors.scrollbarThumb = "#123456";
|
||||
|
||||
const loadedTheme = loadThemeFromPath(writeTheme(themeJson), "truecolor");
|
||||
expect(loadedTheme.getBgAnsi("scrollbarThumb")).toBe("\x1b[48;2;18;52;86m");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue