pi-claude-code-ui 1.0.89 → 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 +21 -0
- package/README.md +27 -9
- package/config/config.example.json +1 -1
- package/extensions/index.ts +318 -34
- 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,27 @@
|
|
|
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
|
+
|
|
21
|
+
## 1.0.90 — 2026-10-19
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- **Split diffs wrap long lines again** - with `"diffLayout": "split"` long lines were cut off with `›` after one row (two or three on wide terminals) instead of wrapping, because split columns kept the wrap limit meant for the auto layout. Forced split now wraps each line in full, up to 8 rows. Thanks @nabsiddiqui ([#34](https://github.com/FammasMaz/pi-cc-tools/issues/34)).
|
|
26
|
+
|
|
6
27
|
## 1.0.89 — 2026-10-19
|
|
7
28
|
|
|
8
29
|
### 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;
|
|
@@ -4238,6 +4336,7 @@ const WORD_DIFF_MIN_SIM = 0.15;
|
|
|
4238
4336
|
const MAX_WRAP_ROWS_WIDE = 3;
|
|
4239
4337
|
const MAX_WRAP_ROWS_MED = 2;
|
|
4240
4338
|
const MAX_WRAP_ROWS_NARROW = 1;
|
|
4339
|
+
const FORCED_SPLIT_WRAP_ROWS = 8;
|
|
4241
4340
|
|
|
4242
4341
|
let D_RST = "\x1b[0m";
|
|
4243
4342
|
const D_BOLD = "\x1b[1m";
|
|
@@ -4259,8 +4358,40 @@ let FG_DEL = "\x1b[38;2;200;100;100m";
|
|
|
4259
4358
|
let FG_DIM = "\x1b[38;2;80;80;80m";
|
|
4260
4359
|
let FG_LNUM = "\x1b[38;2;100;100;100m";
|
|
4261
4360
|
let FG_RULE = "\x1b[38;2;50;50;50m";
|
|
4262
|
-
// 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.
|
|
4263
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
|
+
}
|
|
4264
4395
|
|
|
4265
4396
|
function toolBranchRgbAnsi(gray: number): string {
|
|
4266
4397
|
const g = Math.max(0, Math.min(255, Math.round(gray)));
|
|
@@ -4274,11 +4405,17 @@ function ansiRgbBrightenedBy(ansi: string, delta: number): string | null {
|
|
|
4274
4405
|
return `\x1b[38;2;${bump(rgb.r)};${bump(rgb.g)};${bump(rgb.b)}m`;
|
|
4275
4406
|
}
|
|
4276
4407
|
|
|
4277
|
-
/** Outline chrome always
|
|
4408
|
+
/** Outline chrome always stands out more than the branch; never falls back to identical branch ANSI. */
|
|
4278
4409
|
function outlineChromeAnsiFromBranch(theme?: any): string {
|
|
4279
4410
|
const t = theme ?? _toolBranchThemeHint;
|
|
4411
|
+
if (toolBranchColorModeFixed() && !hasExplicitToolBranchGray()) {
|
|
4412
|
+
const blend = themeChromeBlend(t, OUTLINE_CHROME_CONTRAST);
|
|
4413
|
+
if (blend) return blend;
|
|
4414
|
+
}
|
|
4280
4415
|
const branch = currentToolBranchAnsi(t);
|
|
4281
|
-
|
|
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);
|
|
4282
4419
|
if (fromBranch) return fromBranch;
|
|
4283
4420
|
let gray = DEFAULT_TOOL_BRANCH_GRAY;
|
|
4284
4421
|
if (toolBranchColorModeFixed()) {
|
|
@@ -4301,8 +4438,8 @@ function toolBranchColorModeFixed(): boolean {
|
|
|
4301
4438
|
}
|
|
4302
4439
|
|
|
4303
4440
|
function toolBranchRenderCacheKey(): string {
|
|
4304
|
-
|
|
4305
|
-
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}`;
|
|
4306
4443
|
}
|
|
4307
4444
|
|
|
4308
4445
|
let _toolBranchVisualEpoch = 0;
|
|
@@ -4342,6 +4479,10 @@ let _toolBranchThemeHint: any;
|
|
|
4342
4479
|
function currentToolBranchAnsi(theme?: any): string {
|
|
4343
4480
|
const t = theme ?? _toolBranchThemeHint;
|
|
4344
4481
|
if (toolBranchColorModeFixed()) {
|
|
4482
|
+
if (!hasExplicitToolBranchGray()) {
|
|
4483
|
+
const blend = themeChromeBlend(t, BRANCH_CHROME_CONTRAST);
|
|
4484
|
+
if (blend) return blend;
|
|
4485
|
+
}
|
|
4345
4486
|
return toolBranchRgbAnsi(getConfiguredToolBranchGray());
|
|
4346
4487
|
}
|
|
4347
4488
|
const chrome = t ? resolveThemeChromeFg(t) : null;
|
|
@@ -4491,6 +4632,10 @@ const FALLBACK_BASE_BG_DARK = { r: 32, g: 35, b: 42 };
|
|
|
4491
4632
|
const FALLBACK_BASE_BG_LIGHT = { r: 232, g: 233, b: 236 };
|
|
4492
4633
|
|
|
4493
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";
|
|
4494
4639
|
const panel =
|
|
4495
4640
|
themeBgRgb(theme, "toolSuccessBg") ||
|
|
4496
4641
|
themeBgRgb(theme, "userMessageBg") ||
|
|
@@ -4510,10 +4655,18 @@ function isLightThemeBackground(theme: any): boolean {
|
|
|
4510
4655
|
function syncDiffShikiTheme(theme: any): void {
|
|
4511
4656
|
if (process.env.DIFF_THEME) return;
|
|
4512
4657
|
const config = loadDiffConfig();
|
|
4513
|
-
if (config.diffTheme) return;
|
|
4658
|
+
if (config.diffTheme || config.diffColors?.shikiTheme) return;
|
|
4514
4659
|
_diffOnLightBg = isLightThemeBackground(theme);
|
|
4515
|
-
|
|
4516
|
-
|
|
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;
|
|
4517
4670
|
}
|
|
4518
4671
|
const UNIVERSAL_DIFF_ADD_FG = { r: 110, g: 210, b: 130 };
|
|
4519
4672
|
const UNIVERSAL_DIFF_DEL_FG = { r: 225, g: 110, b: 110 };
|
|
@@ -4534,17 +4687,65 @@ function rgbToBgAnsi(c: { r: number; g: number; b: number }): string {
|
|
|
4534
4687
|
return `\x1b[48;2;${Math.round(c.r)};${Math.round(c.g)};${Math.round(c.b)}m`;
|
|
4535
4688
|
}
|
|
4536
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
|
+
|
|
4537
4729
|
function autoDeriveBgFromTheme(theme: any): void {
|
|
4538
4730
|
// Diff palette derivation.
|
|
4539
4731
|
//
|
|
4540
4732
|
// `toolDiffAdded` / `toolDiffRemoved` from the active pi theme give us the
|
|
4541
|
-
// fg accents.
|
|
4542
|
-
//
|
|
4543
|
-
//
|
|
4544
|
-
//
|
|
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.
|
|
4545
4738
|
const useTheme = themeAdaptiveEnabled() && theme;
|
|
4546
4739
|
const onLight = useTheme && isLightThemeBackground(theme);
|
|
4547
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
|
+
}
|
|
4548
4749
|
const addFgRgb = (useTheme && themeFgRgb(theme, "toolDiffAdded")) || UNIVERSAL_DIFF_ADD_FG;
|
|
4549
4750
|
const delFgRgb = (useTheme && themeFgRgb(theme, "toolDiffRemoved")) || UNIVERSAL_DIFF_DEL_FG;
|
|
4550
4751
|
const base =
|
|
@@ -4633,7 +4834,12 @@ function applyThemePaletteIfNeeded(theme: any): void {
|
|
|
4633
4834
|
}
|
|
4634
4835
|
const paletteChanged =
|
|
4635
4836
|
_themePaletteCacheName !== themeName || _themePaletteCacheFingerprint !== fingerprint;
|
|
4636
|
-
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
|
+
}
|
|
4637
4843
|
_themePaletteCacheTheme = theme;
|
|
4638
4844
|
_themePaletteCacheName = themeName;
|
|
4639
4845
|
_themePaletteCacheFingerprint = fingerprint;
|
|
@@ -5102,7 +5308,7 @@ function lang(filePath: string): BundledLanguage | undefined {
|
|
|
5102
5308
|
return EXT_LANG[extname(filePath).slice(1).toLowerCase()];
|
|
5103
5309
|
}
|
|
5104
5310
|
|
|
5105
|
-
async function codeToAnsiLazy(code: string, language: BundledLanguage, theme: BundledTheme): Promise<string> {
|
|
5311
|
+
async function codeToAnsiLazy(code: string, language: BundledLanguage, theme: BundledTheme | ShikiThemeRegistration): Promise<string> {
|
|
5106
5312
|
if (!codeToAnsiLoader) {
|
|
5107
5313
|
// Self-healing: a failed import (missing dep, transient error) must not leave a
|
|
5108
5314
|
// permanently-rejected promise that later becomes an unhandled rejection. Reset
|
|
@@ -5139,11 +5345,15 @@ function touchCache(key: string, value: string[]): string[] {
|
|
|
5139
5345
|
async function hlBlock(code: string, language: BundledLanguage | undefined): Promise<string[]> {
|
|
5140
5346
|
if (!code) return [""];
|
|
5141
5347
|
if (!language || code.length > MAX_HL_CHARS) return code.split("\n");
|
|
5142
|
-
const
|
|
5348
|
+
const diffTheme = DIFF_THEME;
|
|
5349
|
+
const key = `${diffThemeName(diffTheme)}\0${language}\0${code}`;
|
|
5143
5350
|
const hit = hlCache.get(key);
|
|
5144
5351
|
if (hit) return touchCache(key, hit);
|
|
5145
5352
|
try {
|
|
5146
|
-
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;
|
|
5147
5357
|
const out = (ansi.endsWith("\n") ? ansi.slice(0, -1) : ansi).split("\n");
|
|
5148
5358
|
return touchCache(key, out);
|
|
5149
5359
|
} catch {
|
|
@@ -5416,6 +5626,13 @@ async function renderSplit(
|
|
|
5416
5626
|
const nw = Math.max(2, String(maxLineNumber(diff.lines)).length);
|
|
5417
5627
|
const gw = nw + 5;
|
|
5418
5628
|
const cw = Math.max(12, half - gw);
|
|
5629
|
+
// Auto only picks split when lines fit, so the usual wrap cap rarely bites. A forced
|
|
5630
|
+
// split ("diffLayout": "split") halves the code width for every diff, and that cap
|
|
5631
|
+
// (one row below 120 columns) cut long lines off instead of wrapping them. Wrap
|
|
5632
|
+
// forced-split lines in full, with a cap only against minified one-liners.
|
|
5633
|
+
const splitWrapRows = diffLayout() === "split"
|
|
5634
|
+
? Math.max(adaptiveWrapRows(), FORCED_SPLIT_WRAP_ROWS)
|
|
5635
|
+
: adaptiveWrapRows();
|
|
5419
5636
|
const canHL = diff.chars <= MAX_HL_CHARS && vis.length * 2 <= MAX_RENDER_LINES * 2;
|
|
5420
5637
|
|
|
5421
5638
|
const leftSrc: string[] = [];
|
|
@@ -5465,7 +5682,7 @@ async function renderSplit(
|
|
|
5465
5682
|
else body = `${BG_BASE}${D_DIM}${hl}`;
|
|
5466
5683
|
const gutter = `${border}${gBg}${lnum(num, nw, numFg)}${sFg}${D_BOLD}${sign} ${D_RST}${FG_RULE}│${D_RST} `;
|
|
5467
5684
|
const contGutter = `${border}${gBg}${" ".repeat(nw + 2)}${D_RST}${FG_RULE}│${D_RST} `;
|
|
5468
|
-
return { gutter, contGutter, bodyRows: wrapAnsi(tabs(body), cw,
|
|
5685
|
+
return { gutter, contGutter, bodyRows: wrapAnsi(tabs(body), cw, splitWrapRows, cBg) };
|
|
5469
5686
|
}
|
|
5470
5687
|
|
|
5471
5688
|
const out: string[] = [];
|
|
@@ -7078,9 +7295,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
7078
7295
|
const chromeHint = branchMode === "theme" && theme
|
|
7079
7296
|
? (resolveThemeChromeFg(theme) ? " (attenuated on light themes)" : " (fallback gray if theme keys missing)")
|
|
7080
7297
|
: "";
|
|
7081
|
-
const branchLine = branchMode === "
|
|
7082
|
-
? `Branch color:
|
|
7083
|
-
:
|
|
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)";
|
|
7084
7303
|
ctx.ui.notify([
|
|
7085
7304
|
`Tool style: ${toolBackgroundMode}`,
|
|
7086
7305
|
`Tool grouping: ${toolGroupingEnabled() ? "on" : "off"}`,
|
|
@@ -7170,7 +7389,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7170
7389
|
writeSettingsKey("toolBranchRgbGray", undefined);
|
|
7171
7390
|
writeSettingsKey("toolBranchColorMode", undefined);
|
|
7172
7391
|
if (ctx.hasUI) refreshAllToolBranchVisuals(ctx);
|
|
7173
|
-
if (ctx.hasUI) ctx.ui.notify(
|
|
7392
|
+
if (ctx.hasUI) ctx.ui.notify("Branch color → default (fixed contrast on the terminal background)", "info");
|
|
7174
7393
|
return;
|
|
7175
7394
|
}
|
|
7176
7395
|
if (arg === "theme") {
|
|
@@ -7182,7 +7401,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7182
7401
|
if (arg === "fixed") {
|
|
7183
7402
|
writeSettingsKey("toolBranchColorMode", "fixed");
|
|
7184
7403
|
if (ctx.hasUI) refreshAllToolBranchVisuals(ctx);
|
|
7185
|
-
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");
|
|
7186
7407
|
return;
|
|
7187
7408
|
}
|
|
7188
7409
|
const gray = Number.parseInt(arg, 10);
|
|
@@ -7247,11 +7468,66 @@ export default function (pi: ExtensionAPI) {
|
|
|
7247
7468
|
},
|
|
7248
7469
|
});
|
|
7249
7470
|
|
|
7250
|
-
// /cc-theme command — toggle pi-theme-adaptive coloring at runtime
|
|
7251
|
-
|
|
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
|
+
};
|
|
7252
7514
|
pi.registerCommand("cc-theme", {
|
|
7253
|
-
description: "
|
|
7515
|
+
description: "Follow the terminal's colors (terminal on|off), or toggle whether tool chrome / diff colors follow the pi theme",
|
|
7254
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
|
+
}
|
|
7255
7531
|
return THEME_MODES
|
|
7256
7532
|
.filter((m) => m.startsWith(prefix))
|
|
7257
7533
|
.map((m) => ({
|
|
@@ -7261,11 +7537,16 @@ export default function (pi: ExtensionAPI) {
|
|
|
7261
7537
|
m === "on" ? "Derive borders, branch rules, dim text and diff tints from the active pi theme (default)"
|
|
7262
7538
|
: m === "off" ? "Keep the fixed Claude-style palette regardless of theme"
|
|
7263
7539
|
: m === "toggle" ? "Flip between on and off"
|
|
7540
|
+
: m === "terminal" ? "Make pi's theme follow the terminal's colors (Ghostty, Rex, ...)"
|
|
7264
7541
|
: "Show the current setting and a preview of the derived colors",
|
|
7265
7542
|
}));
|
|
7266
7543
|
},
|
|
7267
7544
|
async handler(args, ctx) {
|
|
7268
7545
|
const raw = args.trim().toLowerCase();
|
|
7546
|
+
if (raw === "terminal" || raw.startsWith("terminal ")) {
|
|
7547
|
+
handleTerminalTheme(raw.slice("terminal".length).trim(), ctx);
|
|
7548
|
+
return;
|
|
7549
|
+
}
|
|
7269
7550
|
const current = themeAdaptiveEnabled();
|
|
7270
7551
|
|
|
7271
7552
|
if (!raw || raw === "status") {
|
|
@@ -7273,6 +7554,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
7273
7554
|
const theme = ctx.ui.theme as any;
|
|
7274
7555
|
const themeName = theme?.name ?? "unknown";
|
|
7275
7556
|
const state = current ? "on" : "off";
|
|
7557
|
+
const terminalLine = `terminal colors: ${terminalThemeEnabled() ? "on" : "off"} (/cc-theme terminal on|off)`;
|
|
7276
7558
|
if (raw === "status" && current) {
|
|
7277
7559
|
const settings = readSettings();
|
|
7278
7560
|
const verbKey = settings.spinnerVerbColor || "borderAccent";
|
|
@@ -7290,9 +7572,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
7290
7572
|
`spinner verb: ${verbAnsi ? `${verbAnsi}Cooking…\x1b[39m` : "(unchanged)"} (key: ${verbKey})`,
|
|
7291
7573
|
`spinner stat: ${statusAnsi ? `${statusAnsi}(thinking · ↓ 10 tokens · 2s)\x1b[39m` : "(unchanged)"} (key: ${statusKey})`,
|
|
7292
7574
|
].join("\n ");
|
|
7293
|
-
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");
|
|
7294
7576
|
} else {
|
|
7295
|
-
ctx.ui.notify(`Theme adaptive: ${state} (theme "${themeName}")`, "info");
|
|
7577
|
+
ctx.ui.notify(`Theme adaptive: ${state} (theme "${themeName}")\n ${terminalLine}`, "info");
|
|
7296
7578
|
}
|
|
7297
7579
|
return;
|
|
7298
7580
|
}
|
|
@@ -7426,6 +7708,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
7426
7708
|
clearRtkRewriteState();
|
|
7427
7709
|
if (!ctx.hasUI) return;
|
|
7428
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 });
|
|
7429
7713
|
// Session switch (/resume, /new) can leave tool chrome from the previous
|
|
7430
7714
|
// theme; rebind from ctx.ui.theme (other extensions may setTheme in the
|
|
7431
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"
|