pi-claude-code-ui 1.0.90 → 1.0.91
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/README.md +27 -9
- package/config/config.example.json +1 -1
- package/extensions/index.ts +309 -33
- package/extensions/terminal-theme.ts +132 -0
- package/extensions/theme-colors.ts +242 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,21 @@
|
|
|
3
3
|
> [!IMPORTANT]
|
|
4
4
|
> **1.0.69 — package rename (permanent).** Canonical npm name is now [`pi-claude-code-ui`](https://www.npmjs.com/package/pi-claude-code-ui). `pi-claude-style-tools` is legacy and will not receive further releases. Install with `pi install npm:pi-claude-code-ui` or `npm i pi-claude-code-ui`.
|
|
5
5
|
|
|
6
|
+
## 1.0.91 — 2026-10-24
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **Follow the terminal's colors** - `/cc-theme terminal on` (setting `terminalTheme`) switches pi to its built-in `system` theme, which takes every color from the palette Ghostty, Rex, or any other terminal reports, and keeps it on at every start. `/cc-theme terminal off` restores the previous theme. Terminals do not announce switches between two themes of the same kind (dark to dark), so the extension also asks the terminal for its color scheme when its window regains focus, and pi re-reads the palette.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **Diffs use the theme's colors** - diff code was highlighted with GitHub's palette (`github-dark` / `github-light`) whatever the pi theme, so it never matched Markdown code blocks or the terminal. Diffs now use the theme's `syntax*` colors, and diff rows tint the terminal background toward the theme's own `toolDiffAdded` / `toolDiffRemoved` instead of mixing in fixed green and pink. Needs pi 1.4 or newer; older versions keep the GitHub palettes.
|
|
15
|
+
- **Connectors and outlines follow the theme's tint** - branch connectors were a fixed neutral rgb(72) and box outlines rgb(136) on every theme, which looked heavy on light themes and gray on tinted ones. The default now blends the terminal background toward its text color to the same contrast (2:1 and 5.1:1), so dark terminals look as before. An explicit `/cc-tools branch <0-255>` still sets an exact gray.
|
|
16
|
+
- **Light and dark agree with pi** - light/dark choices (diff palette, chrome) now come from pi's `theme.appearance` instead of a separate brightness guess, so they can no longer disagree with pi's own UI.
|
|
17
|
+
- **Light themes no longer gray out syntax colors** - the contrast fix for bundled Shiki themes replaced every color above a fixed brightness with gray on light themes. It now only applies to the bundled themes.
|
|
18
|
+
- **`diffColors.shikiTheme` is kept on theme switches** - a light/dark switch overwrote it with `github-dark` / `github-light`.
|
|
19
|
+
- **Transparent tool backgrounds on pi 1.4** - pi 1.4 renamed the theme's background table, so `toolBackground` overrides were silently skipped.
|
|
20
|
+
|
|
6
21
|
## 1.0.90 — 2026-10-19
|
|
7
22
|
|
|
8
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -28,9 +28,9 @@ Claude Code inspired tool rendering for Pi — Shiki-powered diffs, status dots,
|
|
|
28
28
|
- **Subagent completion notifications** restyled to match the same Claude-style tool rows
|
|
29
29
|
- **RTK rewrite integration** that folds rewrite notices into the bash tool row with a muted `(RTK)` badge and expanded-only rewrite details
|
|
30
30
|
- **Transparent tool backgrounds** in `transparent` or `border` mode
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **Transparent edit/write diffs** with
|
|
31
|
+
- **Terminal colors** — `/cc-theme terminal on` makes pi's theme follow the terminal's own palette (Ghostty, Rex, kitty, WezTerm, …), including switches between two dark or two light themes
|
|
32
|
+
- **Theme-adaptive palette** — borders, branch connectors, dim text, spinner accent, diff highlighting, and diff backgrounds automatically follow the active pi theme (set `themeAdaptive: false` to keep the fixed Claude-style palette)
|
|
33
|
+
- **Transparent edit/write diffs** tinted with the theme's own diff colors
|
|
34
34
|
- **Grouped consecutive tool calls** with single-row summaries for repeated targets and per-tool glance rows for mixed work (set `groupToolCalls: false` to disable)
|
|
35
35
|
- **Extra detail toggle** with `Ctrl+Shift+O`, increasing expanded preview caps without making the default view heavy
|
|
36
36
|
- **Global border patch** for all tool rows, including unknown/custom tools
|
|
@@ -59,35 +59,53 @@ Set in `.pi/settings.json` or the active Pi agent settings file (`$PI_CODING_AGE
|
|
|
59
59
|
"diffCollapsedLines": 24,
|
|
60
60
|
"diffLayout": "auto",
|
|
61
61
|
"themeAdaptive": true,
|
|
62
|
-
"
|
|
62
|
+
"terminalTheme": false
|
|
63
63
|
}
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
+
Leave `diffTheme` unset to highlight diffs with the theme's colors; set it (for example `"github-dark"`) to force a fixed diff palette.
|
|
67
|
+
|
|
66
68
|
### Theme integration
|
|
67
69
|
|
|
68
70
|
When `themeAdaptive` is `true` (default), the following colors are derived from the active pi theme on every render and re-derived whenever the theme changes:
|
|
69
71
|
|
|
70
72
|
| Element | Derived from |
|
|
71
73
|
|---------|--------------|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
+
| Branch connectors (`├`, `└`, `│`) | terminal background blended toward its text color to a 2:1 contrast (rgb 72 on a near-black terminal); `/cc-tools branch theme` uses `dim` → `muted` → `borderMuted` instead, `/cc-tools branch <0-255>` an exact gray |
|
|
75
|
+
| User box, tool rules, code fences, thinking rule | the same blend at a 5.1:1 contrast (rgb ~136 on a near-black terminal) |
|
|
74
76
|
| "✻ Turn took Ns" line (final message only, with session total + turn count) | `muted` |
|
|
75
77
|
| Expanded thinking-block text and `∴` marker | `muted` |
|
|
76
78
|
| Diff add/remove accents | `toolDiffAdded` / `toolDiffRemoved` |
|
|
77
|
-
| Diff
|
|
79
|
+
| Diff code highlighting | the theme's `syntax*` colors, the same ones pi uses for Markdown code blocks |
|
|
80
|
+
| Diff background tints | the terminal background tinted toward `toolDiffAdded` / `toolDiffRemoved` |
|
|
81
|
+
| Light or dark variants | pi's own decision (`theme.appearance`) |
|
|
78
82
|
| Spinner verb text (`Working…`) | `borderAccent` (fallback: `accent`) |
|
|
79
83
|
| Spinner status text | `muted` |
|
|
80
84
|
|
|
85
|
+
These need pi 1.4 or newer, which exposes the theme's concrete colors. On older pi versions branch connectors stay rgb 72, diffs use `github-dark` / `github-light`, and diff tints are mixed against `toolSuccessBg`.
|
|
86
|
+
|
|
81
87
|
User-supplied `diffTheme` presets and `diffColors` overrides always win over theme-derived defaults. File-type icons (e.g. `ts`, `py`, `rs`) keep their language-identity colors and are not theme-derived.
|
|
82
88
|
|
|
83
89
|
Set `themeAdaptive: false` to keep the original fixed Claude-style palette regardless of the active pi theme.
|
|
84
90
|
|
|
85
|
-
On `/resume`, `/new`, or `/fork`, tool chrome is rebound from the **current** pi theme
|
|
91
|
+
On `/resume`, `/new`, or `/fork`, tool chrome is rebound from the **current** pi theme, so it also follows themes set by other extensions.
|
|
92
|
+
|
|
93
|
+
### Terminal colors (Ghostty, Rex, …)
|
|
94
|
+
|
|
95
|
+
`/cc-theme terminal on` switches pi to its built-in `system` theme, which builds every pi color from the palette the terminal reports, and keeps it there on every start. The theme you had before is saved as `terminalThemePrevious`; `/cc-theme terminal off` restores it.
|
|
96
|
+
|
|
97
|
+
Pi already rebuilds the `system` theme when the terminal switches between light and dark. Terminals send no notification when you switch between two themes of the same kind, for example from one dark Rex theme to another, so this extension also asks the terminal for its color scheme whenever the terminal window regains focus (you went to the terminal's settings and came back). Pi then re-reads the palette and repaints only if the colors changed. The query is answered by Ghostty, Rex, and other terminals that support color scheme reports; in regular (non-fullscreen) mode the extension turns on focus reporting itself and keeps the focus events out of the editor.
|
|
98
|
+
|
|
99
|
+
```text
|
|
100
|
+
/cc-theme terminal on # follow the terminal's colors
|
|
101
|
+
/cc-theme terminal off # go back to the previous theme
|
|
102
|
+
/cc-theme terminal status # show the current state
|
|
103
|
+
```
|
|
86
104
|
|
|
87
105
|
#### Toggle at runtime with `/cc-theme`
|
|
88
106
|
|
|
89
107
|
```text
|
|
90
|
-
/cc-theme # show current setting + theme name
|
|
108
|
+
/cc-theme # show current setting + theme name (and whether terminal colors are on)
|
|
91
109
|
/cc-theme status # show current setting + color preview (incl. spinner)
|
|
92
110
|
/cc-theme on # follow pi theme
|
|
93
111
|
/cc-theme off # keep fixed Claude palette
|
package/extensions/index.ts
CHANGED
|
@@ -71,6 +71,25 @@ import {
|
|
|
71
71
|
getLastBashOutputLine,
|
|
72
72
|
} from "./bash-command";
|
|
73
73
|
import { getUserSettingsPath } from "./settings-path";
|
|
74
|
+
import {
|
|
75
|
+
blendForContrast,
|
|
76
|
+
mixRgb as mixThemeRgb,
|
|
77
|
+
rgbBgAnsi,
|
|
78
|
+
rgbFgAnsi,
|
|
79
|
+
rgbToHex,
|
|
80
|
+
shikiThemeFromPiTheme,
|
|
81
|
+
terminalDefaultColors,
|
|
82
|
+
themeAppearance,
|
|
83
|
+
themeTokenRgb,
|
|
84
|
+
type ShikiThemeRegistration,
|
|
85
|
+
} from "./theme-colors";
|
|
86
|
+
import {
|
|
87
|
+
activeThemeName,
|
|
88
|
+
requestTerminalColorScheme,
|
|
89
|
+
startFocusResync,
|
|
90
|
+
SYSTEM_THEME_NAME,
|
|
91
|
+
systemThemeAvailable,
|
|
92
|
+
} from "./terminal-theme";
|
|
74
93
|
|
|
75
94
|
const RESET = "\x1b[0m";
|
|
76
95
|
const TRANSPARENT_BG = "\x1b[49m";
|
|
@@ -137,9 +156,18 @@ interface SettingsFile {
|
|
|
137
156
|
* defaults so users keep full control.
|
|
138
157
|
*/
|
|
139
158
|
themeAdaptive?: boolean;
|
|
159
|
+
/**
|
|
160
|
+
* When true, pi uses its built-in `system` theme, which takes every color
|
|
161
|
+
* from the terminal's own palette (Ghostty, Rex, ...), and re-reads that
|
|
162
|
+
* palette when the terminal regains focus so same-kind theme switches
|
|
163
|
+
* (dark to dark) are picked up too. Toggle with `/cc-theme terminal`.
|
|
164
|
+
*/
|
|
165
|
+
terminalTheme?: boolean;
|
|
166
|
+
/** Pi theme that was active before `terminalTheme` was turned on; restored when it is turned off. */
|
|
167
|
+
terminalThemePrevious?: string;
|
|
140
168
|
/**
|
|
141
169
|
* Theme color key used for the spinner verb (e.g. "Cooking…"). Defaults
|
|
142
|
-
* to "
|
|
170
|
+
* to "borderAccent". Useful when the active theme's accent is overloaded for
|
|
143
171
|
* borders, headings, or bash mode and the verb should pop differently.
|
|
144
172
|
* Valid keys are any of the pi theme `ThemeColor` names (e.g. accent,
|
|
145
173
|
* borderAccent, success, warning, mdHeading, thinkingMedium, bashMode).
|
|
@@ -157,9 +185,13 @@ interface SettingsFile {
|
|
|
157
185
|
* renders expanded, like stock pi.
|
|
158
186
|
*/
|
|
159
187
|
thinkingMode?: "live" | "full";
|
|
160
|
-
/**
|
|
188
|
+
/** Exact gray level 0–255 for ├ └ │ in `fixed` mode. Unset: a fixed-contrast blend of the terminal background and text. */
|
|
161
189
|
toolBranchRgbGray?: number;
|
|
162
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* `fixed` (default): the terminal background blended toward its text color
|
|
192
|
+
* to a fixed 2:1 contrast (rgb 72 on a #161616 terminal), or exactly
|
|
193
|
+
* `toolBranchRgbGray` when set. `theme`: the theme's dim → muted → borderMuted color.
|
|
194
|
+
*/
|
|
163
195
|
toolBranchColorMode?: "theme" | "fixed";
|
|
164
196
|
}
|
|
165
197
|
|
|
@@ -233,7 +265,10 @@ function syncToolBackgroundMode(): void {
|
|
|
233
265
|
|
|
234
266
|
function setThemeBg(theme: unknown, key: string, value: string): void {
|
|
235
267
|
const themeAny = theme as any;
|
|
236
|
-
|
|
268
|
+
// pi >= 1.4 keeps background escapes in `bgAnsi`; older versions in `bgColors`.
|
|
269
|
+
if (themeAny.bgAnsi instanceof Map) {
|
|
270
|
+
themeAny.bgAnsi.set(key, value);
|
|
271
|
+
} else if (themeAny.bgColors instanceof Map) {
|
|
237
272
|
themeAny.bgColors.set(key, value);
|
|
238
273
|
} else if (themeAny.bgColors && typeof themeAny.bgColors === "object") {
|
|
239
274
|
themeAny.bgColors[key] = value;
|
|
@@ -4203,8 +4238,11 @@ function currentThemePaletteFingerprint(): string | undefined {
|
|
|
4203
4238
|
|
|
4204
4239
|
/** Resolved-color fingerprint so palette re-derives when the active theme file changes under the same name/object. */
|
|
4205
4240
|
function themePaletteFingerprint(theme: any): string {
|
|
4206
|
-
const keys = ["success", "error", "borderMuted", "accent", "muted", "toolDiffAdded", "toolDiffRemoved"] as const;
|
|
4207
|
-
|
|
4241
|
+
const keys = ["success", "error", "borderMuted", "accent", "muted", "toolDiffAdded", "toolDiffRemoved", "syntaxKeyword", "syntaxString"] as const;
|
|
4242
|
+
// Tokens set to "" follow the terminal's default colors, which can change under the same theme.
|
|
4243
|
+
const defaults = terminalDefaultColors(theme);
|
|
4244
|
+
const terminal = defaults ? `${rgbToHex(defaults.background)}${rgbToHex(defaults.foreground)}` : "";
|
|
4245
|
+
return [...keys.map((k) => safeFgAnsi(theme, k) ?? ""), themeAppearance(theme) ?? "", terminal].join("\u001f");
|
|
4208
4246
|
}
|
|
4209
4247
|
|
|
4210
4248
|
function invalidateThemePaletteCache(): void {
|
|
@@ -4218,7 +4256,67 @@ function themeAdaptiveEnabled(): boolean {
|
|
|
4218
4256
|
return settings.themeAdaptive !== false;
|
|
4219
4257
|
}
|
|
4220
4258
|
|
|
4221
|
-
|
|
4259
|
+
// ---------------------------------------------------------------------------
|
|
4260
|
+
// terminalTheme: pi follows the terminal's own palette (Ghostty, Rex, ...).
|
|
4261
|
+
// ---------------------------------------------------------------------------
|
|
4262
|
+
|
|
4263
|
+
function terminalThemeEnabled(): boolean {
|
|
4264
|
+
return readSettings().terminalTheme === true;
|
|
4265
|
+
}
|
|
4266
|
+
|
|
4267
|
+
/** Pi's saved `theme` setting (may be a "light/dark" pair), from pi's own settings file. */
|
|
4268
|
+
function readPiThemeSetting(): string | undefined {
|
|
4269
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR || `${process.env.HOME ?? ""}/.pi/agent`;
|
|
4270
|
+
try {
|
|
4271
|
+
const raw = JSON.parse(readFileSync(`${agentDir}/settings.json`, "utf8"));
|
|
4272
|
+
return typeof raw?.theme === "string" && raw.theme ? raw.theme : undefined;
|
|
4273
|
+
} catch {
|
|
4274
|
+
return undefined;
|
|
4275
|
+
}
|
|
4276
|
+
}
|
|
4277
|
+
|
|
4278
|
+
/** Switch pi to its terminal-palette theme and keep it in sync. Returns false if unavailable. */
|
|
4279
|
+
function applyTerminalTheme(ctx: any, options: { announce: boolean }): boolean {
|
|
4280
|
+
if (!ctx?.hasUI) return false;
|
|
4281
|
+
if (!systemThemeAvailable(ctx.ui)) {
|
|
4282
|
+
ctx.ui.notify("Terminal colors need pi's built-in `system` theme (pi 1.4 or newer).", "error");
|
|
4283
|
+
return false;
|
|
4284
|
+
}
|
|
4285
|
+
const current = activeThemeName(ctx.ui);
|
|
4286
|
+
if (current !== SYSTEM_THEME_NAME) {
|
|
4287
|
+
// Remember what to go back to; a "light/dark" pair is kept as the pair.
|
|
4288
|
+
const previous = readPiThemeSetting() ?? current;
|
|
4289
|
+
if (previous && previous !== SYSTEM_THEME_NAME) writeSettingsKey("terminalThemePrevious", previous);
|
|
4290
|
+
const result = ctx.ui.setTheme(SYSTEM_THEME_NAME);
|
|
4291
|
+
if (!result?.success) {
|
|
4292
|
+
ctx.ui.notify(`Could not switch to the terminal theme: ${result?.error ?? "unknown error"}`, "error");
|
|
4293
|
+
return false;
|
|
4294
|
+
}
|
|
4295
|
+
if (options.announce) {
|
|
4296
|
+
ctx.ui.notify(`Theme follows the terminal's colors (was "${previous ?? current}"). /cc-theme terminal off to switch back.`, "info");
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
startFocusResync(ctx.ui, terminalThemeEnabled);
|
|
4300
|
+
return true;
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
/** Turn terminalTheme off and restore the theme that was active before it. */
|
|
4304
|
+
function restorePreviousTheme(ctx: any): string | undefined {
|
|
4305
|
+
const previous = readSettings().terminalThemePrevious;
|
|
4306
|
+
writeSettingsKey("terminalThemePrevious", undefined);
|
|
4307
|
+
if (!ctx?.hasUI || !previous || activeThemeName(ctx.ui) !== SYSTEM_THEME_NAME) return undefined;
|
|
4308
|
+
let name = previous;
|
|
4309
|
+
if (previous.includes("/")) {
|
|
4310
|
+
// ctx.ui.setTheme takes one theme name; pick the pair member for the current appearance.
|
|
4311
|
+
const [light, dark] = previous.split("/");
|
|
4312
|
+
name = themeAppearance(ctx.ui.theme) === "light" ? light : dark;
|
|
4313
|
+
}
|
|
4314
|
+
const result = ctx.ui.setTheme(name);
|
|
4315
|
+
return result?.success ? name : undefined;
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
/** Shiki theme for diff previews: a bundled theme name, or one generated from the pi theme. */
|
|
4319
|
+
let DIFF_THEME: BundledTheme | ShikiThemeRegistration = (process.env.DIFF_THEME as BundledTheme | undefined) ?? "github-dark";
|
|
4222
4320
|
/** True when the active pi theme has a light panel background (edit/write diff chrome). */
|
|
4223
4321
|
let _diffOnLightBg = false;
|
|
4224
4322
|
let codeToAnsiLoader: Promise<any> | null = null;
|
|
@@ -4260,8 +4358,40 @@ let FG_DEL = "\x1b[38;2;200;100;100m";
|
|
|
4260
4358
|
let FG_DIM = "\x1b[38;2;80;80;80m";
|
|
4261
4359
|
let FG_LNUM = "\x1b[38;2;100;100;100m";
|
|
4262
4360
|
let FG_RULE = "\x1b[38;2;50;50;50m";
|
|
4263
|
-
// Tool branch connectors (├ └ │).
|
|
4361
|
+
// Tool branch connectors (├ └ │). The default ("fixed" mode without an explicit gray) blends the
|
|
4362
|
+
// terminal's background toward its text color until it has the contrast rgb 72 has on a #161616
|
|
4363
|
+
// terminal (2.0:1), so connectors keep that weight on light and tinted themes and take on the
|
|
4364
|
+
// theme's tint. Outline chrome (user box, code fences, thinking rule) does the same at the
|
|
4365
|
+
// contrast of rgb 136 (5.1:1). Gray 72 / +64 is the fallback when pi does not expose concrete
|
|
4366
|
+
// theme colors (pi < 1.4) or themeAdaptive is off.
|
|
4264
4367
|
const DEFAULT_TOOL_BRANCH_GRAY = 72;
|
|
4368
|
+
const BRANCH_CHROME_CONTRAST = 2.0;
|
|
4369
|
+
const OUTLINE_CHROME_CONTRAST = 5.1;
|
|
4370
|
+
|
|
4371
|
+
// Branch connectors resolve their color on every rendered row, so keep the solved blends.
|
|
4372
|
+
const _chromeBlendCache = new Map<string, string>();
|
|
4373
|
+
|
|
4374
|
+
/** Terminal background blended toward its text color to `contrast`, or null without concrete colors. */
|
|
4375
|
+
function themeChromeBlend(theme: any, contrast: number): string | null {
|
|
4376
|
+
if (!theme || !themeAdaptiveEnabled()) return null;
|
|
4377
|
+
const defaults = terminalDefaultColors(theme);
|
|
4378
|
+
if (!defaults) return null;
|
|
4379
|
+
const { background: bg, foreground: fg } = defaults;
|
|
4380
|
+
const mode = typeof theme.getColorMode === "function" ? theme.getColorMode() : "";
|
|
4381
|
+
const key = `${contrast}|${bg.r},${bg.g},${bg.b}|${fg.r},${fg.g},${fg.b}|${mode}`;
|
|
4382
|
+
let ansi = _chromeBlendCache.get(key);
|
|
4383
|
+
if (!ansi) {
|
|
4384
|
+
if (_chromeBlendCache.size > 32) _chromeBlendCache.clear();
|
|
4385
|
+
ansi = rgbFgAnsi(theme, blendForContrast(bg, fg, contrast));
|
|
4386
|
+
_chromeBlendCache.set(key, ansi);
|
|
4387
|
+
}
|
|
4388
|
+
return ansi;
|
|
4389
|
+
}
|
|
4390
|
+
|
|
4391
|
+
function hasExplicitToolBranchGray(): boolean {
|
|
4392
|
+
const raw = readSettings().toolBranchRgbGray;
|
|
4393
|
+
return typeof raw === "number" && Number.isFinite(raw);
|
|
4394
|
+
}
|
|
4265
4395
|
|
|
4266
4396
|
function toolBranchRgbAnsi(gray: number): string {
|
|
4267
4397
|
const g = Math.max(0, Math.min(255, Math.round(gray)));
|
|
@@ -4275,11 +4405,17 @@ function ansiRgbBrightenedBy(ansi: string, delta: number): string | null {
|
|
|
4275
4405
|
return `\x1b[38;2;${bump(rgb.r)};${bump(rgb.g)};${bump(rgb.b)}m`;
|
|
4276
4406
|
}
|
|
4277
4407
|
|
|
4278
|
-
/** Outline chrome always
|
|
4408
|
+
/** Outline chrome always stands out more than the branch; never falls back to identical branch ANSI. */
|
|
4279
4409
|
function outlineChromeAnsiFromBranch(theme?: any): string {
|
|
4280
4410
|
const t = theme ?? _toolBranchThemeHint;
|
|
4411
|
+
if (toolBranchColorModeFixed() && !hasExplicitToolBranchGray()) {
|
|
4412
|
+
const blend = themeChromeBlend(t, OUTLINE_CHROME_CONTRAST);
|
|
4413
|
+
if (blend) return blend;
|
|
4414
|
+
}
|
|
4281
4415
|
const branch = currentToolBranchAnsi(t);
|
|
4282
|
-
|
|
4416
|
+
// Toward the text color: brighter on dark themes, darker on light ones.
|
|
4417
|
+
const towardText = t && isLightThemeBackground(t) ? -OUTLINE_CHROME_BRIGHTEN : OUTLINE_CHROME_BRIGHTEN;
|
|
4418
|
+
const fromBranch = ansiRgbBrightenedBy(branch, towardText);
|
|
4283
4419
|
if (fromBranch) return fromBranch;
|
|
4284
4420
|
let gray = DEFAULT_TOOL_BRANCH_GRAY;
|
|
4285
4421
|
if (toolBranchColorModeFixed()) {
|
|
@@ -4302,8 +4438,8 @@ function toolBranchColorModeFixed(): boolean {
|
|
|
4302
4438
|
}
|
|
4303
4439
|
|
|
4304
4440
|
function toolBranchRenderCacheKey(): string {
|
|
4305
|
-
|
|
4306
|
-
return
|
|
4441
|
+
// The escape itself, not stripAnsi(TOOL_RULE) (which is always ""), so a new color is a new key.
|
|
4442
|
+
return `${toolBranchColorModeFixed() ? "fixed" : "theme"}:${TOOL_RULE}`;
|
|
4307
4443
|
}
|
|
4308
4444
|
|
|
4309
4445
|
let _toolBranchVisualEpoch = 0;
|
|
@@ -4343,6 +4479,10 @@ let _toolBranchThemeHint: any;
|
|
|
4343
4479
|
function currentToolBranchAnsi(theme?: any): string {
|
|
4344
4480
|
const t = theme ?? _toolBranchThemeHint;
|
|
4345
4481
|
if (toolBranchColorModeFixed()) {
|
|
4482
|
+
if (!hasExplicitToolBranchGray()) {
|
|
4483
|
+
const blend = themeChromeBlend(t, BRANCH_CHROME_CONTRAST);
|
|
4484
|
+
if (blend) return blend;
|
|
4485
|
+
}
|
|
4346
4486
|
return toolBranchRgbAnsi(getConfiguredToolBranchGray());
|
|
4347
4487
|
}
|
|
4348
4488
|
const chrome = t ? resolveThemeChromeFg(t) : null;
|
|
@@ -4492,6 +4632,10 @@ const FALLBACK_BASE_BG_DARK = { r: 32, g: 35, b: 42 };
|
|
|
4492
4632
|
const FALLBACK_BASE_BG_LIGHT = { r: 232, g: 233, b: 236 };
|
|
4493
4633
|
|
|
4494
4634
|
function isLightThemeBackground(theme: any): boolean {
|
|
4635
|
+
// pi >= 1.4 decides light/dark itself (from the terminal's colors for the system theme);
|
|
4636
|
+
// use the same answer so diffs, chrome, and pi's own UI never disagree.
|
|
4637
|
+
const appearance = themeAppearance(theme);
|
|
4638
|
+
if (appearance) return appearance === "light";
|
|
4495
4639
|
const panel =
|
|
4496
4640
|
themeBgRgb(theme, "toolSuccessBg") ||
|
|
4497
4641
|
themeBgRgb(theme, "userMessageBg") ||
|
|
@@ -4511,10 +4655,18 @@ function isLightThemeBackground(theme: any): boolean {
|
|
|
4511
4655
|
function syncDiffShikiTheme(theme: any): void {
|
|
4512
4656
|
if (process.env.DIFF_THEME) return;
|
|
4513
4657
|
const config = loadDiffConfig();
|
|
4514
|
-
if (config.diffTheme) return;
|
|
4658
|
+
if (config.diffTheme || config.diffColors?.shikiTheme) return;
|
|
4515
4659
|
_diffOnLightBg = isLightThemeBackground(theme);
|
|
4516
|
-
|
|
4517
|
-
|
|
4660
|
+
// Highlight diffs with the theme's own syntax colors (the ones pi uses for Markdown code
|
|
4661
|
+
// blocks). GitHub's palette is the fallback for pi versions without concrete theme colors.
|
|
4662
|
+
const fromTheme = themeAdaptiveEnabled() ? shikiThemeFromPiTheme(theme) : null;
|
|
4663
|
+
const next = fromTheme ?? ((_diffOnLightBg ? "github-light" : "github-dark") as BundledTheme);
|
|
4664
|
+
if (diffThemeName(next) !== diffThemeName(DIFF_THEME)) clearHighlightCache();
|
|
4665
|
+
DIFF_THEME = next;
|
|
4666
|
+
}
|
|
4667
|
+
|
|
4668
|
+
function diffThemeName(theme: BundledTheme | ShikiThemeRegistration): string {
|
|
4669
|
+
return typeof theme === "string" ? theme : theme.name;
|
|
4518
4670
|
}
|
|
4519
4671
|
const UNIVERSAL_DIFF_ADD_FG = { r: 110, g: 210, b: 130 };
|
|
4520
4672
|
const UNIVERSAL_DIFF_DEL_FG = { r: 225, g: 110, b: 110 };
|
|
@@ -4535,17 +4687,65 @@ function rgbToBgAnsi(c: { r: number; g: number; b: number }): string {
|
|
|
4535
4687
|
return `\x1b[48;2;${Math.round(c.r)};${Math.round(c.g)};${Math.round(c.b)}m`;
|
|
4536
4688
|
}
|
|
4537
4689
|
|
|
4690
|
+
// Diff row tints: how far each background moves from the surface toward the theme's diff color.
|
|
4691
|
+
// Light themes need less, because the same share of a mid-tone color reads stronger on white.
|
|
4692
|
+
const DIFF_TINT = {
|
|
4693
|
+
dark: { line: { add: 0.2, del: 0.17 }, word: { add: 0.36, del: 0.32 }, gutter: { add: 0.12, del: 0.1 } },
|
|
4694
|
+
light: { line: { add: 0.16, del: 0.13 }, word: { add: 0.3, del: 0.26 }, gutter: { add: 0.1, del: 0.08 } },
|
|
4695
|
+
} as const;
|
|
4696
|
+
|
|
4697
|
+
/** The color a diff row is drawn on: the terminal background, or pi's tool panel in `default` tool style. */
|
|
4698
|
+
function diffSurfaceRgb(theme: any): Rgb | null {
|
|
4699
|
+
if (toolBackgroundMode === "default") {
|
|
4700
|
+
const panel = themeTokenRgb(theme, "toolSuccessBg");
|
|
4701
|
+
if (panel) return panel;
|
|
4702
|
+
}
|
|
4703
|
+
return terminalDefaultColors(theme)?.background ?? null;
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
/**
|
|
4707
|
+
* Diff colors straight from the theme: text in `toolDiffAdded` / `toolDiffRemoved`, and
|
|
4708
|
+
* backgrounds that tint the row's real surface toward those same colors. Returns false on pi
|
|
4709
|
+
* versions without concrete theme colors.
|
|
4710
|
+
*/
|
|
4711
|
+
function deriveDiffPaletteFromThemeColors(theme: any): boolean {
|
|
4712
|
+
const add = themeTokenRgb(theme, "toolDiffAdded");
|
|
4713
|
+
const del = themeTokenRgb(theme, "toolDiffRemoved");
|
|
4714
|
+
const surface = diffSurfaceRgb(theme);
|
|
4715
|
+
if (!add || !del || !surface) return false;
|
|
4716
|
+
const tint = DIFF_TINT[isLightThemeBackground(theme) ? "light" : "dark"];
|
|
4717
|
+
const bg = (color: Rgb, amount: number) => rgbBgAnsi(theme, mixThemeRgb(surface, color, amount));
|
|
4718
|
+
FG_ADD = rgbFgAnsi(theme, add);
|
|
4719
|
+
FG_DEL = rgbFgAnsi(theme, del);
|
|
4720
|
+
BG_ADD = bg(add, tint.line.add);
|
|
4721
|
+
BG_DEL = bg(del, tint.line.del);
|
|
4722
|
+
BG_ADD_W = bg(add, tint.word.add);
|
|
4723
|
+
BG_DEL_W = bg(del, tint.word.del);
|
|
4724
|
+
BG_GUTTER_ADD = bg(add, tint.gutter.add);
|
|
4725
|
+
BG_GUTTER_DEL = bg(del, tint.gutter.del);
|
|
4726
|
+
return true;
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4538
4729
|
function autoDeriveBgFromTheme(theme: any): void {
|
|
4539
4730
|
// Diff palette derivation.
|
|
4540
4731
|
//
|
|
4541
4732
|
// `toolDiffAdded` / `toolDiffRemoved` from the active pi theme give us the
|
|
4542
|
-
// fg accents.
|
|
4543
|
-
//
|
|
4544
|
-
//
|
|
4545
|
-
//
|
|
4733
|
+
// fg accents. On pi >= 1.4 the backgrounds tint the row's real surface toward
|
|
4734
|
+
// those colors (deriveDiffPaletteFromThemeColors). Older pi versions only
|
|
4735
|
+
// expose ANSI escapes, so the base is taken from `toolSuccessBg` and mixed
|
|
4736
|
+
// with fixed tint targets. Falls back to the universal dark palette when the
|
|
4737
|
+
// theme is unavailable or themeAdaptive=false.
|
|
4546
4738
|
const useTheme = themeAdaptiveEnabled() && theme;
|
|
4547
4739
|
const onLight = useTheme && isLightThemeBackground(theme);
|
|
4548
4740
|
_diffOnLightBg = !!onLight;
|
|
4741
|
+
if (useTheme && deriveDiffPaletteFromThemeColors(theme)) {
|
|
4742
|
+
BG_EMPTY = TRANSPARENT_BG;
|
|
4743
|
+
BG_BASE = TRANSPARENT_BG;
|
|
4744
|
+
D_RST = TRANSPARENT_RESET;
|
|
4745
|
+
DIVIDER = `${FG_RULE}│${D_RST}`;
|
|
4746
|
+
DEFAULT_DIFF_COLORS = { fgAdd: FG_ADD, fgDel: FG_DEL, fgCtx: FG_DIM };
|
|
4747
|
+
return;
|
|
4748
|
+
}
|
|
4549
4749
|
const addFgRgb = (useTheme && themeFgRgb(theme, "toolDiffAdded")) || UNIVERSAL_DIFF_ADD_FG;
|
|
4550
4750
|
const delFgRgb = (useTheme && themeFgRgb(theme, "toolDiffRemoved")) || UNIVERSAL_DIFF_DEL_FG;
|
|
4551
4751
|
const base =
|
|
@@ -4634,7 +4834,12 @@ function applyThemePaletteIfNeeded(theme: any): void {
|
|
|
4634
4834
|
}
|
|
4635
4835
|
const paletteChanged =
|
|
4636
4836
|
_themePaletteCacheName !== themeName || _themePaletteCacheFingerprint !== fingerprint;
|
|
4637
|
-
if (paletteChanged)
|
|
4837
|
+
if (paletteChanged) {
|
|
4838
|
+
bumpToolBranchVisualEpoch();
|
|
4839
|
+
// A new theme object (e.g. the system theme rebuilt after a terminal theme change)
|
|
4840
|
+
// comes with pi's own tool backgrounds; re-apply the configured tool style to it.
|
|
4841
|
+
applyToolBackgroundMode(theme);
|
|
4842
|
+
}
|
|
4638
4843
|
_themePaletteCacheTheme = theme;
|
|
4639
4844
|
_themePaletteCacheName = themeName;
|
|
4640
4845
|
_themePaletteCacheFingerprint = fingerprint;
|
|
@@ -5103,7 +5308,7 @@ function lang(filePath: string): BundledLanguage | undefined {
|
|
|
5103
5308
|
return EXT_LANG[extname(filePath).slice(1).toLowerCase()];
|
|
5104
5309
|
}
|
|
5105
5310
|
|
|
5106
|
-
async function codeToAnsiLazy(code: string, language: BundledLanguage, theme: BundledTheme): Promise<string> {
|
|
5311
|
+
async function codeToAnsiLazy(code: string, language: BundledLanguage, theme: BundledTheme | ShikiThemeRegistration): Promise<string> {
|
|
5107
5312
|
if (!codeToAnsiLoader) {
|
|
5108
5313
|
// Self-healing: a failed import (missing dep, transient error) must not leave a
|
|
5109
5314
|
// permanently-rejected promise that later becomes an unhandled rejection. Reset
|
|
@@ -5140,11 +5345,15 @@ function touchCache(key: string, value: string[]): string[] {
|
|
|
5140
5345
|
async function hlBlock(code: string, language: BundledLanguage | undefined): Promise<string[]> {
|
|
5141
5346
|
if (!code) return [""];
|
|
5142
5347
|
if (!language || code.length > MAX_HL_CHARS) return code.split("\n");
|
|
5143
|
-
const
|
|
5348
|
+
const diffTheme = DIFF_THEME;
|
|
5349
|
+
const key = `${diffThemeName(diffTheme)}\0${language}\0${code}`;
|
|
5144
5350
|
const hit = hlCache.get(key);
|
|
5145
5351
|
if (hit) return touchCache(key, hit);
|
|
5146
5352
|
try {
|
|
5147
|
-
const
|
|
5353
|
+
const raw = await codeToAnsiLazy(code, language, diffTheme as any);
|
|
5354
|
+
// Themes generated from pi already meet pi's contrast rules for this background;
|
|
5355
|
+
// only bundled themes need their too-dark / too-light colors muted.
|
|
5356
|
+
const ansi = typeof diffTheme === "string" ? normalizeShikiContrast(raw) : raw;
|
|
5148
5357
|
const out = (ansi.endsWith("\n") ? ansi.slice(0, -1) : ansi).split("\n");
|
|
5149
5358
|
return touchCache(key, out);
|
|
5150
5359
|
} catch {
|
|
@@ -7086,9 +7295,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
7086
7295
|
const chromeHint = branchMode === "theme" && theme
|
|
7087
7296
|
? (resolveThemeChromeFg(theme) ? " (attenuated on light themes)" : " (fallback gray if theme keys missing)")
|
|
7088
7297
|
: "";
|
|
7089
|
-
const branchLine = branchMode === "
|
|
7090
|
-
? `Branch color:
|
|
7091
|
-
:
|
|
7298
|
+
const branchLine = branchMode === "theme"
|
|
7299
|
+
? `Branch color: theme${chromeHint}`
|
|
7300
|
+
: hasExplicitToolBranchGray() || !themeChromeBlend(theme, BRANCH_CHROME_CONTRAST)
|
|
7301
|
+
? `Branch color: fixed rgb(${branchGray})`
|
|
7302
|
+
: "Branch color: fixed 2:1 contrast on the terminal background (rgb 72 on near-black)";
|
|
7092
7303
|
ctx.ui.notify([
|
|
7093
7304
|
`Tool style: ${toolBackgroundMode}`,
|
|
7094
7305
|
`Tool grouping: ${toolGroupingEnabled() ? "on" : "off"}`,
|
|
@@ -7178,7 +7389,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7178
7389
|
writeSettingsKey("toolBranchRgbGray", undefined);
|
|
7179
7390
|
writeSettingsKey("toolBranchColorMode", undefined);
|
|
7180
7391
|
if (ctx.hasUI) refreshAllToolBranchVisuals(ctx);
|
|
7181
|
-
if (ctx.hasUI) ctx.ui.notify(
|
|
7392
|
+
if (ctx.hasUI) ctx.ui.notify("Branch color → default (fixed contrast on the terminal background)", "info");
|
|
7182
7393
|
return;
|
|
7183
7394
|
}
|
|
7184
7395
|
if (arg === "theme") {
|
|
@@ -7190,7 +7401,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7190
7401
|
if (arg === "fixed") {
|
|
7191
7402
|
writeSettingsKey("toolBranchColorMode", "fixed");
|
|
7192
7403
|
if (ctx.hasUI) refreshAllToolBranchVisuals(ctx);
|
|
7193
|
-
if (ctx.hasUI) ctx.ui.notify(
|
|
7404
|
+
if (ctx.hasUI) ctx.ui.notify(hasExplicitToolBranchGray()
|
|
7405
|
+
? `Branch color → fixed rgb(${getConfiguredToolBranchGray()})`
|
|
7406
|
+
: "Branch color → fixed contrast on the terminal background", "info");
|
|
7194
7407
|
return;
|
|
7195
7408
|
}
|
|
7196
7409
|
const gray = Number.parseInt(arg, 10);
|
|
@@ -7255,11 +7468,66 @@ export default function (pi: ExtensionAPI) {
|
|
|
7255
7468
|
},
|
|
7256
7469
|
});
|
|
7257
7470
|
|
|
7258
|
-
// /cc-theme command — toggle pi-theme-adaptive coloring at runtime
|
|
7259
|
-
|
|
7471
|
+
// /cc-theme command — toggle pi-theme-adaptive coloring at runtime, and
|
|
7472
|
+
// /cc-theme terminal — make pi's theme follow the terminal's palette.
|
|
7473
|
+
const THEME_MODES = ["on", "off", "toggle", "status", "terminal"] as const;
|
|
7474
|
+
const TERMINAL_MODES = ["on", "off", "toggle", "status"] as const;
|
|
7475
|
+
const handleTerminalTheme = (arg: string, ctx: any): void => {
|
|
7476
|
+
const current = terminalThemeEnabled();
|
|
7477
|
+
if (!arg || arg === "status") {
|
|
7478
|
+
if (!ctx.hasUI) return;
|
|
7479
|
+
const name = activeThemeName(ctx.ui) ?? "unknown";
|
|
7480
|
+
const previous = readSettings().terminalThemePrevious;
|
|
7481
|
+
ctx.ui.notify([
|
|
7482
|
+
`Terminal colors: ${current ? "on" : "off"} (pi theme "${name}")`,
|
|
7483
|
+
current && previous ? ` /cc-theme terminal off restores "${previous}"` : "",
|
|
7484
|
+
current && name !== SYSTEM_THEME_NAME ? " pi is not using the system theme right now; /cc-theme terminal on switches it back" : "",
|
|
7485
|
+
].filter(Boolean).join("\n"), "info");
|
|
7486
|
+
return;
|
|
7487
|
+
}
|
|
7488
|
+
let next: boolean;
|
|
7489
|
+
if (arg === "on") next = true;
|
|
7490
|
+
else if (arg === "off") next = false;
|
|
7491
|
+
else if (arg === "toggle") next = !current;
|
|
7492
|
+
else {
|
|
7493
|
+
if (ctx.hasUI) ctx.ui.notify(`Usage: /cc-theme terminal ${TERMINAL_MODES.join("|")}`, "error");
|
|
7494
|
+
return;
|
|
7495
|
+
}
|
|
7496
|
+
if (next) {
|
|
7497
|
+
writeSettingsKey("terminalTheme", true);
|
|
7498
|
+
if (!applyTerminalTheme(ctx, { announce: false })) {
|
|
7499
|
+
writeSettingsKey("terminalTheme", undefined);
|
|
7500
|
+
return;
|
|
7501
|
+
}
|
|
7502
|
+
requestTerminalColorScheme();
|
|
7503
|
+
if (ctx.hasUI) ctx.ui.notify("Terminal colors: on — pi's theme now follows the terminal (Ghostty, Rex, ...)", "info");
|
|
7504
|
+
} else {
|
|
7505
|
+
writeSettingsKey("terminalTheme", undefined);
|
|
7506
|
+
const restored = restorePreviousTheme(ctx);
|
|
7507
|
+
if (ctx.hasUI) {
|
|
7508
|
+
ctx.ui.notify(restored
|
|
7509
|
+
? `Terminal colors: off — restored theme "${restored}"`
|
|
7510
|
+
: "Terminal colors: off — pick a theme with /settings", "info");
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
};
|
|
7260
7514
|
pi.registerCommand("cc-theme", {
|
|
7261
|
-
description: "
|
|
7515
|
+
description: "Follow the terminal's colors (terminal on|off), or toggle whether tool chrome / diff colors follow the pi theme",
|
|
7262
7516
|
getArgumentCompletions(prefix) {
|
|
7517
|
+
if (prefix.startsWith("terminal ")) {
|
|
7518
|
+
const second = prefix.slice("terminal ".length);
|
|
7519
|
+
return TERMINAL_MODES
|
|
7520
|
+
.filter((m) => m.startsWith(second))
|
|
7521
|
+
.map((m) => ({
|
|
7522
|
+
value: `terminal ${m}`,
|
|
7523
|
+
label: m,
|
|
7524
|
+
description:
|
|
7525
|
+
m === "on" ? "Use pi's system theme, built from the terminal's palette, and re-read it on focus"
|
|
7526
|
+
: m === "off" ? "Go back to the theme you had before"
|
|
7527
|
+
: m === "toggle" ? "Flip between on and off"
|
|
7528
|
+
: "Show whether pi follows the terminal's colors",
|
|
7529
|
+
}));
|
|
7530
|
+
}
|
|
7263
7531
|
return THEME_MODES
|
|
7264
7532
|
.filter((m) => m.startsWith(prefix))
|
|
7265
7533
|
.map((m) => ({
|
|
@@ -7269,11 +7537,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
7269
7537
|
m === "on" ? "Derive borders, branch rules, dim text and diff tints from the active pi theme (default)"
|
|
7270
7538
|
: m === "off" ? "Keep the fixed Claude-style palette regardless of theme"
|
|
7271
7539
|
: m === "toggle" ? "Flip between on and off"
|
|
7540
|
+
: m === "terminal" ? "Make pi's theme follow the terminal's colors (Ghostty, Rex, ...)"
|
|
7272
7541
|
: "Show the current setting and a preview of the derived colors",
|
|
7273
7542
|
}));
|
|
7274
7543
|
},
|
|
7275
7544
|
async handler(args, ctx) {
|
|
7276
7545
|
const raw = args.trim().toLowerCase();
|
|
7546
|
+
if (raw === "terminal" || raw.startsWith("terminal ")) {
|
|
7547
|
+
handleTerminalTheme(raw.slice("terminal".length).trim(), ctx);
|
|
7548
|
+
return;
|
|
7549
|
+
}
|
|
7277
7550
|
const current = themeAdaptiveEnabled();
|
|
7278
7551
|
|
|
7279
7552
|
if (!raw || raw === "status") {
|
|
@@ -7281,6 +7554,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7281
7554
|
const theme = ctx.ui.theme as any;
|
|
7282
7555
|
const themeName = theme?.name ?? "unknown";
|
|
7283
7556
|
const state = current ? "on" : "off";
|
|
7557
|
+
const terminalLine = `terminal colors: ${terminalThemeEnabled() ? "on" : "off"} (/cc-theme terminal on|off)`;
|
|
7284
7558
|
if (raw === "status" && current) {
|
|
7285
7559
|
const settings = readSettings();
|
|
7286
7560
|
const verbKey = settings.spinnerVerbColor || "borderAccent";
|
|
@@ -7298,9 +7572,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7298
7572
|
`spinner verb: ${verbAnsi ? `${verbAnsi}Cooking…\x1b[39m` : "(unchanged)"} (key: ${verbKey})`,
|
|
7299
7573
|
`spinner stat: ${statusAnsi ? `${statusAnsi}(thinking · ↓ 10 tokens · 2s)\x1b[39m` : "(unchanged)"} (key: ${statusKey})`,
|
|
7300
7574
|
].join("\n ");
|
|
7301
|
-
ctx.ui.notify(`Theme adaptive: ${state} (theme "${themeName}")\n ${preview}`, "info");
|
|
7575
|
+
ctx.ui.notify(`Theme adaptive: ${state} (theme "${themeName}")\n ${terminalLine}\n ${preview}`, "info");
|
|
7302
7576
|
} else {
|
|
7303
|
-
ctx.ui.notify(`Theme adaptive: ${state} (theme "${themeName}")`, "info");
|
|
7577
|
+
ctx.ui.notify(`Theme adaptive: ${state} (theme "${themeName}")\n ${terminalLine}`, "info");
|
|
7304
7578
|
}
|
|
7305
7579
|
return;
|
|
7306
7580
|
}
|
|
@@ -7434,6 +7708,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7434
7708
|
clearRtkRewriteState();
|
|
7435
7709
|
if (!ctx.hasUI) return;
|
|
7436
7710
|
patchUiNotifications(ctx.ui);
|
|
7711
|
+
// terminalTheme: switch to (and stay on) the theme built from the terminal's palette.
|
|
7712
|
+
if (terminalThemeEnabled()) applyTerminalTheme(ctx, { announce: true });
|
|
7437
7713
|
// Session switch (/resume, /new) can leave tool chrome from the previous
|
|
7438
7714
|
// theme; rebind from ctx.ui.theme (other extensions may setTheme in the
|
|
7439
7715
|
// same tick — deferred passes pick up the final theme without coupling).
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// Follow the terminal's own colors (Ghostty, Rex, kitty, WezTerm, ...).
|
|
2
|
+
//
|
|
3
|
+
// Pi's built-in `system` theme already builds every color from the palette the
|
|
4
|
+
// terminal reports (OSC 4/10/11) and rebuilds it when the terminal announces a
|
|
5
|
+
// light/dark switch (DEC mode 2031). Terminals announce nothing when you switch
|
|
6
|
+
// between two themes of the same kind, for example from one dark Rex theme to
|
|
7
|
+
// another. So whenever the terminal regains focus (you went to its settings and
|
|
8
|
+
// came back) we ask it for its color scheme (DSR 996). Pi's own handler treats
|
|
9
|
+
// the reply like a light/dark report and re-reads the palette, and it only
|
|
10
|
+
// re-renders when the colors actually changed.
|
|
11
|
+
|
|
12
|
+
const FOCUS_IN = "\x1b[I";
|
|
13
|
+
const FOCUS_OUT = "\x1b[O";
|
|
14
|
+
const ENABLE_FOCUS_REPORTING = "\x1b[?1004h";
|
|
15
|
+
const DISABLE_FOCUS_REPORTING = "\x1b[?1004l";
|
|
16
|
+
const REQUEST_COLOR_SCHEME = "\x1b[?996n";
|
|
17
|
+
const MIN_REQUEST_INTERVAL_MS = 250;
|
|
18
|
+
|
|
19
|
+
export const SYSTEM_THEME_NAME = "system";
|
|
20
|
+
|
|
21
|
+
export interface TerminalIo {
|
|
22
|
+
/** Raw terminal input. Defaults to process.stdin. */
|
|
23
|
+
input: { on(event: "data", listener: (chunk: unknown) => void): unknown };
|
|
24
|
+
/** Writes escape sequences to the terminal. Defaults to process.stdout when it is a TTY. */
|
|
25
|
+
write(data: string): void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface FocusResyncState {
|
|
29
|
+
io?: TerminalIo;
|
|
30
|
+
stdinListener?: (chunk: unknown) => void;
|
|
31
|
+
unsubscribeInput?: () => void;
|
|
32
|
+
exitHook?: () => void;
|
|
33
|
+
lastRequestAt: number;
|
|
34
|
+
isEnabled: () => boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Kept on globalThis so /reload (which re-evaluates this module) replaces the
|
|
38
|
+
// previous listeners instead of stacking them.
|
|
39
|
+
const STATE_KEY = Symbol.for("pi-claude-style-tools:terminal-theme-focus");
|
|
40
|
+
|
|
41
|
+
function state(): FocusResyncState {
|
|
42
|
+
const g = globalThis as any;
|
|
43
|
+
if (!g[STATE_KEY]) g[STATE_KEY] = { lastRequestAt: 0, isEnabled: () => false } satisfies FocusResyncState;
|
|
44
|
+
return g[STATE_KEY] as FocusResyncState;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const processIo: TerminalIo = {
|
|
48
|
+
input: process.stdin,
|
|
49
|
+
write(data: string): void {
|
|
50
|
+
try {
|
|
51
|
+
if (process.stdout.isTTY) process.stdout.write(data);
|
|
52
|
+
} catch { /* terminal gone */ }
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function write(data: string): void {
|
|
57
|
+
(state().io ?? processIo).write(data);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Ask the terminal to report its light/dark scheme; pi re-reads the palette on the reply. */
|
|
61
|
+
export function requestTerminalColorScheme(): void {
|
|
62
|
+
const s = state();
|
|
63
|
+
const now = Date.now();
|
|
64
|
+
if (now - s.lastRequestAt < MIN_REQUEST_INTERVAL_MS) return;
|
|
65
|
+
s.lastRequestAt = now;
|
|
66
|
+
write(REQUEST_COLOR_SCHEME);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Re-read the terminal palette whenever the terminal regains focus.
|
|
71
|
+
*
|
|
72
|
+
* Fullscreen pi already turns on focus reporting and consumes the focus events
|
|
73
|
+
* before extensions see them, so focus-in is detected by watching stdin
|
|
74
|
+
* directly (without consuming anything). In regular mode we turn focus
|
|
75
|
+
* reporting on ourselves and consume the events so they never reach the editor.
|
|
76
|
+
*/
|
|
77
|
+
export function startFocusResync(ui: any, isEnabled: () => boolean, io: TerminalIo = processIo): void {
|
|
78
|
+
const s = state();
|
|
79
|
+
s.isEnabled = isEnabled;
|
|
80
|
+
if (s.io && s.io !== io) {
|
|
81
|
+
// A different terminal (tests): start over instead of mixing listeners.
|
|
82
|
+
s.stdinListener = undefined;
|
|
83
|
+
s.exitHook = undefined;
|
|
84
|
+
}
|
|
85
|
+
s.io = io;
|
|
86
|
+
|
|
87
|
+
if (!s.stdinListener) {
|
|
88
|
+
s.stdinListener = (chunk: unknown) => {
|
|
89
|
+
const text = typeof chunk === "string" ? chunk : Buffer.isBuffer(chunk) ? chunk.toString("latin1") : "";
|
|
90
|
+
if (text.includes(FOCUS_IN) && state().isEnabled()) requestTerminalColorScheme();
|
|
91
|
+
};
|
|
92
|
+
io.input.on("data", s.stdinListener);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Pi drops extension input listeners on /reload and session replacement; re-register each time.
|
|
96
|
+
s.unsubscribeInput?.();
|
|
97
|
+
s.unsubscribeInput = undefined;
|
|
98
|
+
if (typeof ui?.onTerminalInput === "function") {
|
|
99
|
+
try {
|
|
100
|
+
s.unsubscribeInput = ui.onTerminalInput((data: string) =>
|
|
101
|
+
data === FOCUS_IN || data === FOCUS_OUT ? { consume: true } : undefined,
|
|
102
|
+
);
|
|
103
|
+
} catch { /* not interactive */ }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
write(ENABLE_FOCUS_REPORTING);
|
|
107
|
+
if (!s.exitHook && io === processIo) {
|
|
108
|
+
s.exitHook = () => write(DISABLE_FOCUS_REPORTING);
|
|
109
|
+
process.once("exit", s.exitHook);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Theme name pi currently uses ("system" when it follows the terminal). */
|
|
114
|
+
export function activeThemeName(ui: any): string | undefined {
|
|
115
|
+
try {
|
|
116
|
+
const name = ui?.theme?.name;
|
|
117
|
+
return typeof name === "string" ? name : undefined;
|
|
118
|
+
} catch {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** True when this pi version has the built-in terminal-palette theme. */
|
|
124
|
+
export function systemThemeAvailable(ui: any): boolean {
|
|
125
|
+
try {
|
|
126
|
+
if (activeThemeName(ui) === SYSTEM_THEME_NAME) return true;
|
|
127
|
+
const themes = ui?.getAllThemes?.();
|
|
128
|
+
return Array.isArray(themes) && themes.some((t: any) => t?.name === SYSTEM_THEME_NAME);
|
|
129
|
+
} catch {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// Concrete colors from the active pi theme, and a Shiki theme built from them.
|
|
2
|
+
//
|
|
3
|
+
// Pi >= 1.4 exposes `theme.appearance` ("dark" | "light") and `theme.colors`
|
|
4
|
+
// (concrete colors for every token, with tokens set to "" filled from the
|
|
5
|
+
// terminal's reported default colors). Older pi versions only expose ANSI
|
|
6
|
+
// escapes, so every helper here returns null when that API is missing and the
|
|
7
|
+
// callers fall back to parsing `getFgAnsi()` / `getBgAnsi()`.
|
|
8
|
+
|
|
9
|
+
import * as PiTui from "@earendil-works/pi-tui";
|
|
10
|
+
|
|
11
|
+
export type Rgb = { r: number; g: number; b: number };
|
|
12
|
+
|
|
13
|
+
export function rgbToHex({ r, g, b }: Rgb): string {
|
|
14
|
+
const h = (n: number) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
15
|
+
return `#${h(r)}${h(g)}${h(b)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Linear sRGB mix: `amount` 0 returns `a`, 1 returns `b`. */
|
|
19
|
+
export function mixRgb(a: Rgb, b: Rgb, amount: number): Rgb {
|
|
20
|
+
return {
|
|
21
|
+
r: a.r + (b.r - a.r) * amount,
|
|
22
|
+
g: a.g + (b.g - a.g) * amount,
|
|
23
|
+
b: a.b + (b.b - a.b) * amount,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function relativeLuminance({ r, g, b }: Rgb): number {
|
|
28
|
+
const lin = (c: number) => {
|
|
29
|
+
const v = c / 255;
|
|
30
|
+
return v <= 0.04045 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4;
|
|
31
|
+
};
|
|
32
|
+
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** WCAG contrast ratio, 1-21. */
|
|
36
|
+
export function contrastRatio(a: Rgb, b: Rgb): number {
|
|
37
|
+
const la = relativeLuminance(a);
|
|
38
|
+
const lb = relativeLuminance(b);
|
|
39
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The color between `background` and `foreground` that has `ratio` contrast
|
|
44
|
+
* against the background, so chrome keeps the same visual weight on dark,
|
|
45
|
+
* light, and low-contrast themes. Returns the foreground if it cannot reach it.
|
|
46
|
+
*/
|
|
47
|
+
export function blendForContrast(background: Rgb, foreground: Rgb, ratio: number): Rgb {
|
|
48
|
+
if (contrastRatio(background, foreground) <= ratio) return foreground;
|
|
49
|
+
let lo = 0;
|
|
50
|
+
let hi = 1;
|
|
51
|
+
for (let i = 0; i < 18; i++) {
|
|
52
|
+
const mid = (lo + hi) / 2;
|
|
53
|
+
if (contrastRatio(background, mixRgb(background, foreground, mid)) < ratio) lo = mid;
|
|
54
|
+
else hi = mid;
|
|
55
|
+
}
|
|
56
|
+
return mixRgb(background, foreground, hi);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Pi Color value ({ kind: "rgb" | "oklch" | "indexed", ... }) to RGB. */
|
|
60
|
+
function colorValueToRgb(color: any): Rgb | null {
|
|
61
|
+
if (!color || typeof color !== "object") return null;
|
|
62
|
+
const toRgb = (PiTui as any).colorToRgb;
|
|
63
|
+
if (typeof toRgb === "function") {
|
|
64
|
+
try {
|
|
65
|
+
const rgb = toRgb(color);
|
|
66
|
+
if (rgb && Number.isFinite(rgb.r)) return { r: rgb.r, g: rgb.g, b: rgb.b };
|
|
67
|
+
} catch { /* fall through */ }
|
|
68
|
+
}
|
|
69
|
+
if (color.kind === "rgb" && Number.isFinite(color.r)) return { r: color.r, g: color.g, b: color.b };
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Concrete RGB for a theme token, or null on pi versions without `theme.colors`. */
|
|
74
|
+
export function themeTokenRgb(theme: any, token: string): Rgb | null {
|
|
75
|
+
try {
|
|
76
|
+
return colorValueToRgb(theme?.colors?.[token]);
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** "light" / "dark" as pi decides it, or undefined on pi versions without `theme.appearance`. */
|
|
83
|
+
export function themeAppearance(theme: any): "light" | "dark" | undefined {
|
|
84
|
+
try {
|
|
85
|
+
const appearance = theme?.appearance;
|
|
86
|
+
return appearance === "light" || appearance === "dark" ? appearance : undefined;
|
|
87
|
+
} catch {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A copy of the active theme with every token set to "" (terminal default).
|
|
93
|
+
// Pi fills those from the terminal's reported default colors, which is the
|
|
94
|
+
// only public way to read the terminal background. Built from the active
|
|
95
|
+
// theme's own class so it reads the same module state as pi.
|
|
96
|
+
const probeCache = new WeakMap<object, any>();
|
|
97
|
+
|
|
98
|
+
function terminalDefaultsProbe(theme: any): any | null {
|
|
99
|
+
try {
|
|
100
|
+
const fg = theme?.fgAnsi;
|
|
101
|
+
const bg = theme?.bgAnsi;
|
|
102
|
+
const Ctor = theme?.constructor;
|
|
103
|
+
if (!(fg instanceof Map) || !(bg instanceof Map) || typeof Ctor !== "function") return null;
|
|
104
|
+
const key = fg as object;
|
|
105
|
+
let probe = probeCache.get(key);
|
|
106
|
+
if (!probe) {
|
|
107
|
+
const blank = (map: Map<string, unknown>) => Object.fromEntries([...map.keys()].map((k) => [k, ""]));
|
|
108
|
+
const mode = typeof theme.getColorMode === "function" ? theme.getColorMode() : "truecolor";
|
|
109
|
+
probe = new Ctor(blank(fg), blank(bg), mode, { appearance: themeAppearance(theme) });
|
|
110
|
+
probeCache.set(key, probe);
|
|
111
|
+
}
|
|
112
|
+
return probe;
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The terminal's default background and foreground as pi knows them (reported
|
|
120
|
+
* by the terminal, or pi's guess for the theme's appearance). Null on pi
|
|
121
|
+
* versions without `theme.colors`.
|
|
122
|
+
*/
|
|
123
|
+
export function terminalDefaultColors(theme: any): { background: Rgb; foreground: Rgb } | null {
|
|
124
|
+
const probe = terminalDefaultsProbe(theme);
|
|
125
|
+
if (!probe) return null;
|
|
126
|
+
const background = themeTokenRgb(probe, "userMessageBg");
|
|
127
|
+
const foreground = themeTokenRgb(probe, "text");
|
|
128
|
+
return background && foreground ? { background, foreground } : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** ANSI foreground escape for an RGB color in the theme's color mode (truecolor or 256). */
|
|
132
|
+
export function rgbFgAnsi(theme: any, c: Rgb): string {
|
|
133
|
+
const mode = typeof theme?.getColorMode === "function" ? theme.getColorMode() : "truecolor";
|
|
134
|
+
const fgAnsi = (PiTui as any).foregroundAnsi;
|
|
135
|
+
const rgbColor = (PiTui as any).rgbColor;
|
|
136
|
+
if (mode !== "truecolor" && typeof fgAnsi === "function" && typeof rgbColor === "function") {
|
|
137
|
+
try {
|
|
138
|
+
return fgAnsi(rgbColor(Math.round(c.r), Math.round(c.g), Math.round(c.b)), mode);
|
|
139
|
+
} catch { /* fall through */ }
|
|
140
|
+
}
|
|
141
|
+
return `\x1b[38;2;${Math.round(c.r)};${Math.round(c.g)};${Math.round(c.b)}m`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** ANSI background escape for an RGB color in the theme's color mode (truecolor or 256). */
|
|
145
|
+
export function rgbBgAnsi(theme: any, c: Rgb): string {
|
|
146
|
+
const mode = typeof theme?.getColorMode === "function" ? theme.getColorMode() : "truecolor";
|
|
147
|
+
const bgAnsi = (PiTui as any).backgroundAnsi;
|
|
148
|
+
const rgbColor = (PiTui as any).rgbColor;
|
|
149
|
+
if (mode !== "truecolor" && typeof bgAnsi === "function" && typeof rgbColor === "function") {
|
|
150
|
+
try {
|
|
151
|
+
return bgAnsi(rgbColor(Math.round(c.r), Math.round(c.g), Math.round(c.b)), mode);
|
|
152
|
+
} catch { /* fall through */ }
|
|
153
|
+
}
|
|
154
|
+
return `\x1b[48;2;${Math.round(c.r)};${Math.round(c.g)};${Math.round(c.b)}m`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Shiki theme from the pi theme's syntax colors, so diff previews highlight
|
|
159
|
+
// code the same way pi highlights Markdown code blocks. The scope mapping
|
|
160
|
+
// mirrors pi's own highlight.js mapping (keyword, type, number, string,
|
|
161
|
+
// comment, function, variable, operator, punctuation).
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
export interface ShikiThemeRegistration {
|
|
165
|
+
name: string;
|
|
166
|
+
type: "dark" | "light";
|
|
167
|
+
fg: string;
|
|
168
|
+
bg: string;
|
|
169
|
+
settings: Array<{ scope?: string[]; settings: { foreground?: string; background?: string; fontStyle?: string } }>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const SHIKI_SCOPES: Array<[token: string, scopes: string[], fontStyle?: string]> = [
|
|
173
|
+
["syntaxComment", ["comment", "punctuation.definition.comment", "string.comment"]],
|
|
174
|
+
["syntaxKeyword", [
|
|
175
|
+
"keyword", "storage", "storage.type", "storage.modifier", "keyword.control",
|
|
176
|
+
"variable.language", "entity.name.tag", "support.type.property-name.json",
|
|
177
|
+
]],
|
|
178
|
+
["syntaxOperator", ["keyword.operator", "keyword.operator.assignment", "keyword.operator.arithmetic", "keyword.operator.logical", "keyword.operator.comparison"]],
|
|
179
|
+
["syntaxPunctuation", ["punctuation", "meta.brace", "meta.delimiter", "punctuation.definition.tag", "punctuation.separator", "punctuation.terminator"]],
|
|
180
|
+
["syntaxString", ["string", "string.quoted", "string.template", "string.regexp"]],
|
|
181
|
+
["syntaxNumber", ["constant.numeric", "constant.language", "constant.character", "constant.other", "support.constant"]],
|
|
182
|
+
["syntaxFunction", ["entity.name.function", "support.function", "meta.function-call.generic", "variable.function"]],
|
|
183
|
+
["syntaxType", [
|
|
184
|
+
"entity.name.type", "entity.name.class", "entity.name.namespace", "entity.other.inherited-class",
|
|
185
|
+
"support.type", "support.class", "storage.type.primitive", "storage.type.built-in",
|
|
186
|
+
]],
|
|
187
|
+
["syntaxVariable", ["variable.parameter", "entity.other.attribute-name", "meta.object-literal.key"]],
|
|
188
|
+
["mdHeading", ["markup.heading", "entity.name.section"], "bold"],
|
|
189
|
+
["mdLink", ["markup.underline.link", "string.other.link"]],
|
|
190
|
+
["mdCode", ["markup.inline.raw", "markup.inline.raw.string", "markup.fenced_code", "markup.raw"]],
|
|
191
|
+
["mdListBullet", ["punctuation.definition.list"]],
|
|
192
|
+
["toolDiffAdded", ["markup.inserted"]],
|
|
193
|
+
["toolDiffRemoved", ["markup.deleted"]],
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
let shikiCache: { key: string; theme: ShikiThemeRegistration } | null = null;
|
|
197
|
+
|
|
198
|
+
/** Shiki theme from the active pi theme, or null when concrete colors are unavailable. */
|
|
199
|
+
export function shikiThemeFromPiTheme(theme: any): ShikiThemeRegistration | null {
|
|
200
|
+
const defaults = terminalDefaultColors(theme);
|
|
201
|
+
const text = themeTokenRgb(theme, "text") ?? defaults?.foreground ?? null;
|
|
202
|
+
const appearance = themeAppearance(theme);
|
|
203
|
+
if (!text || !appearance) return null;
|
|
204
|
+
const background = defaults?.background ?? (appearance === "light" ? { r: 255, g: 255, b: 255 } : { r: 0, g: 0, b: 0 });
|
|
205
|
+
|
|
206
|
+
const entries: Array<[string, string]> = [];
|
|
207
|
+
for (const [token] of SHIKI_SCOPES) {
|
|
208
|
+
const rgb = themeTokenRgb(theme, token);
|
|
209
|
+
if (rgb) entries.push([token, rgbToHex(rgb)]);
|
|
210
|
+
}
|
|
211
|
+
if (entries.length === 0) return null;
|
|
212
|
+
const colors = Object.fromEntries(entries) as Record<string, string>;
|
|
213
|
+
const fg = rgbToHex(text);
|
|
214
|
+
const bg = rgbToHex(background);
|
|
215
|
+
const key = `${appearance}|${fg}|${bg}|${entries.map(([k, v]) => `${k}=${v}`).join(",")}`;
|
|
216
|
+
if (shikiCache?.key === key) return shikiCache.theme;
|
|
217
|
+
|
|
218
|
+
let hash = 2166136261;
|
|
219
|
+
for (let i = 0; i < key.length; i++) {
|
|
220
|
+
hash ^= key.charCodeAt(i);
|
|
221
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
222
|
+
}
|
|
223
|
+
const settings: ShikiThemeRegistration["settings"] = [{ settings: { foreground: fg, background: bg } }];
|
|
224
|
+
for (const [token, scopes, fontStyle] of SHIKI_SCOPES) {
|
|
225
|
+
const color = colors[token];
|
|
226
|
+
if (!color) continue;
|
|
227
|
+
settings.push({ scope: scopes, settings: fontStyle ? { foreground: color, fontStyle } : { foreground: color } });
|
|
228
|
+
}
|
|
229
|
+
settings.push({ scope: ["markup.bold"], settings: { fontStyle: "bold" } });
|
|
230
|
+
settings.push({ scope: ["markup.italic"], settings: { fontStyle: "italic" } });
|
|
231
|
+
|
|
232
|
+
const registration: ShikiThemeRegistration = {
|
|
233
|
+
// Shiki caches themes by name, so the name changes with the colors.
|
|
234
|
+
name: `pi-theme-${hash.toString(16).padStart(8, "0")}`,
|
|
235
|
+
type: appearance,
|
|
236
|
+
fg,
|
|
237
|
+
bg,
|
|
238
|
+
settings,
|
|
239
|
+
};
|
|
240
|
+
shikiCache = { key, theme: registration };
|
|
241
|
+
return registration;
|
|
242
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-claude-code-ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.91",
|
|
4
4
|
"description": "Claude Code-style grouped tool rows for pi with Ctrl+O/Ctrl+Shift+O previews, math rendering, and consistent built-in, MCP, and custom tool rendering",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
"extensions/spinner.ts",
|
|
33
33
|
"extensions/settings-path.ts",
|
|
34
34
|
"extensions/bash-command.ts",
|
|
35
|
+
"extensions/theme-colors.ts",
|
|
36
|
+
"extensions/terminal-theme.ts",
|
|
35
37
|
"config/config.example.json",
|
|
36
38
|
"README.md",
|
|
37
39
|
"CHANGELOG.md"
|