gentle-pi 0.9.2 → 0.10.0
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/extensions/gentle-ai.ts +129 -39
- package/lib/terminal-theme.ts +16 -0
- package/package.json +5 -1
- package/tests/gentle-ai.test.ts +34 -0
- package/tests/gentle-theme.test.ts +221 -0
- package/tests/runtime-harness.mjs +16 -7
- package/tests/terminal-theme.test.ts +22 -0
- package/themes/Gentle.json +100 -0
package/extensions/gentle-ai.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
import type {
|
|
20
20
|
ExtensionAPI,
|
|
21
21
|
ExtensionContext,
|
|
22
|
+
Theme,
|
|
23
|
+
ThemeColor,
|
|
22
24
|
ToolCallEventResult,
|
|
23
25
|
} from "@earendil-works/pi-coding-agent";
|
|
24
26
|
import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
@@ -45,6 +47,7 @@ import {
|
|
|
45
47
|
type ChangedDiff,
|
|
46
48
|
type TriggerEvent,
|
|
47
49
|
} from "../lib/review-triggers.ts";
|
|
50
|
+
import { sanitizeTerminalText, stripAnsi } from "../lib/terminal-theme.ts";
|
|
48
51
|
|
|
49
52
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
50
53
|
const ASSETS_DIR = join(PACKAGE_ROOT, "assets");
|
|
@@ -713,17 +716,8 @@ function isThinkingLevel(value: unknown): value is ThinkingLevel {
|
|
|
713
716
|
);
|
|
714
717
|
}
|
|
715
718
|
|
|
716
|
-
const ANSI_ESCAPE_PATTERN =
|
|
717
|
-
/[\u001b\u009b][[\]()#;?]*(?:(?:(?:[a-zA-Z\d]*(?:;[a-zA-Z\d]*)*)?\u0007)|(?:(?:\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-nq-uy=><~]))/g;
|
|
718
|
-
const CONTROL_CHAR_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
|
|
719
719
|
const SAFE_MODEL_ID_PATTERN = /^[A-Za-z0-9._~:@/+%-]+$/;
|
|
720
720
|
|
|
721
|
-
function sanitizeTerminalText(value: string): string {
|
|
722
|
-
return value
|
|
723
|
-
.replace(ANSI_ESCAPE_PATTERN, "")
|
|
724
|
-
.replace(CONTROL_CHAR_PATTERN, "");
|
|
725
|
-
}
|
|
726
|
-
|
|
727
721
|
function normalizeModelId(value: unknown): string | undefined {
|
|
728
722
|
if (typeof value !== "string") return undefined;
|
|
729
723
|
const model = value.trim();
|
|
@@ -1236,6 +1230,26 @@ type ModelPanelResult =
|
|
|
1236
1230
|
|
|
1237
1231
|
const SET_ALL_AGENTS = "Set all agents";
|
|
1238
1232
|
|
|
1233
|
+
const PANEL_TONE = {
|
|
1234
|
+
BORDER: "border",
|
|
1235
|
+
MUTED: "muted",
|
|
1236
|
+
TEXT: "text",
|
|
1237
|
+
TITLE: "title",
|
|
1238
|
+
ACCENT: "accent",
|
|
1239
|
+
STATUS: "status",
|
|
1240
|
+
} as const;
|
|
1241
|
+
|
|
1242
|
+
type PanelTone = (typeof PANEL_TONE)[keyof typeof PANEL_TONE];
|
|
1243
|
+
|
|
1244
|
+
const PANEL_TONE_COLOR: Record<PanelTone, ThemeColor> = {
|
|
1245
|
+
border: "border",
|
|
1246
|
+
muted: "muted",
|
|
1247
|
+
text: "text",
|
|
1248
|
+
title: "accent",
|
|
1249
|
+
accent: "accent",
|
|
1250
|
+
status: "thinkingHigh",
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1239
1253
|
class SddModelPanel implements OverlayComponent {
|
|
1240
1254
|
private cursor = 0;
|
|
1241
1255
|
private mode: "agents" | "models" | "effort" = "agents";
|
|
@@ -1247,17 +1261,20 @@ class SddModelPanel implements OverlayComponent {
|
|
|
1247
1261
|
private readonly rows: string[];
|
|
1248
1262
|
private readonly modelOptions: string[];
|
|
1249
1263
|
private readonly done: (result: ModelPanelResult) => void;
|
|
1264
|
+
private readonly theme: Theme | undefined;
|
|
1250
1265
|
|
|
1251
1266
|
constructor(
|
|
1252
1267
|
initialConfig: AgentModelConfig,
|
|
1253
1268
|
modelOptions: string[],
|
|
1254
1269
|
agents: string[],
|
|
1255
1270
|
done: (result: ModelPanelResult) => void,
|
|
1271
|
+
theme?: Theme,
|
|
1256
1272
|
) {
|
|
1257
1273
|
this.draft = cloneModelConfig(initialConfig);
|
|
1258
1274
|
this.rows = [SET_ALL_AGENTS, ...agents];
|
|
1259
1275
|
this.modelOptions = modelOptions;
|
|
1260
1276
|
this.done = done;
|
|
1277
|
+
this.theme = theme;
|
|
1261
1278
|
}
|
|
1262
1279
|
|
|
1263
1280
|
invalidate(): void {}
|
|
@@ -1287,15 +1304,46 @@ class SddModelPanel implements OverlayComponent {
|
|
|
1287
1304
|
|
|
1288
1305
|
private renderCard(lines: string[], width: number): string[] {
|
|
1289
1306
|
const innerWidth = Math.max(1, width - 4);
|
|
1290
|
-
const
|
|
1291
|
-
|
|
1307
|
+
const horizontal = "─".repeat(innerWidth + 2);
|
|
1308
|
+
const border = (text: string) => this.renderText(text, "border");
|
|
1292
1309
|
return [
|
|
1293
|
-
`╭${
|
|
1294
|
-
...lines.map(
|
|
1295
|
-
|
|
1310
|
+
border(`╭${horizontal}╮`),
|
|
1311
|
+
...lines.map(
|
|
1312
|
+
(line) =>
|
|
1313
|
+
`${border("│")} ${this.fitStyledLine(line, innerWidth)} ${border("│")}`,
|
|
1314
|
+
),
|
|
1315
|
+
border(`╰${horizontal}╯`),
|
|
1296
1316
|
];
|
|
1297
1317
|
}
|
|
1298
1318
|
|
|
1319
|
+
private fitStyledLine(line: string, width: number): string {
|
|
1320
|
+
const visible = stripAnsi(line);
|
|
1321
|
+
if (visible.length > width) {
|
|
1322
|
+
return truncateToWidth(visible, Math.max(1, width), "…", true);
|
|
1323
|
+
}
|
|
1324
|
+
return `${line}${" ".repeat(Math.max(0, width - visible.length))}`;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
private renderLine(text = "", width: number, tone?: PanelTone): string {
|
|
1328
|
+
const safe = truncateToWidth(
|
|
1329
|
+
sanitizeTerminalText(text),
|
|
1330
|
+
Math.max(1, width),
|
|
1331
|
+
"…",
|
|
1332
|
+
true,
|
|
1333
|
+
);
|
|
1334
|
+
return tone ? this.renderText(safe, tone) : safe;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private renderText(text: string, tone: PanelTone): string {
|
|
1338
|
+
const safe = sanitizeTerminalText(text);
|
|
1339
|
+
if (!this.theme) return safe;
|
|
1340
|
+
return this.theme.fg(PANEL_TONE_COLOR[tone], safe);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
private renderCursor(focused: boolean): string {
|
|
1344
|
+
return focused ? this.renderText("▸", "accent") : " ";
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1299
1347
|
private handleAgentInput(data: string): void {
|
|
1300
1348
|
const maxCursor = this.rows.length + 1;
|
|
1301
1349
|
if (matchesKey(data, "ctrl+c") || matchesKey(data, "escape")) {
|
|
@@ -1478,11 +1526,11 @@ class SddModelPanel implements OverlayComponent {
|
|
|
1478
1526
|
|
|
1479
1527
|
private renderAgentList(width: number): string[] {
|
|
1480
1528
|
const lines: string[] = [];
|
|
1481
|
-
const line = (text = "") =>
|
|
1482
|
-
|
|
1483
|
-
lines.push(line("Assign Models and Effort to Agents"));
|
|
1529
|
+
const line = (text = "", tone?: PanelTone) =>
|
|
1530
|
+
this.renderLine(text, width, tone);
|
|
1531
|
+
lines.push(line("Assign Models and Effort to Agents", "title"));
|
|
1484
1532
|
lines.push("");
|
|
1485
|
-
lines.push(line("Current assignments:"));
|
|
1533
|
+
lines.push(line("Current assignments:", "muted"));
|
|
1486
1534
|
lines.push("");
|
|
1487
1535
|
const visibleRows = Math.min(AGENT_LIST_MAX_VISIBLE_ROWS, this.rows.length);
|
|
1488
1536
|
const listCursor = Math.min(this.cursor, this.rows.length - 1);
|
|
@@ -1494,7 +1542,7 @@ class SddModelPanel implements OverlayComponent {
|
|
|
1494
1542
|
),
|
|
1495
1543
|
);
|
|
1496
1544
|
const end = Math.min(this.rows.length, start + visibleRows);
|
|
1497
|
-
if (start > 0) lines.push(line(` ↑ ${start} more agent(s)
|
|
1545
|
+
if (start > 0) lines.push(line(` ↑ ${start} more agent(s)`, "muted"));
|
|
1498
1546
|
for (let i = start; i < end; i++) {
|
|
1499
1547
|
const row = this.rows[i] ?? SET_ALL_AGENTS;
|
|
1500
1548
|
const focused = i === this.cursor;
|
|
@@ -1502,21 +1550,28 @@ class SddModelPanel implements OverlayComponent {
|
|
|
1502
1550
|
row === SET_ALL_AGENTS
|
|
1503
1551
|
? this.renderSetAllLabel(row)
|
|
1504
1552
|
: this.renderAgentLabel(row);
|
|
1505
|
-
lines.push(
|
|
1553
|
+
lines.push(`${this.renderCursor(focused)} ${label}`);
|
|
1506
1554
|
}
|
|
1507
1555
|
if (end < this.rows.length)
|
|
1508
|
-
lines.push(line(` ↓ ${this.rows.length - end} more agent(s)
|
|
1556
|
+
lines.push(line(` ↓ ${this.rows.length - end} more agent(s)`, "muted"));
|
|
1509
1557
|
lines.push("");
|
|
1510
1558
|
lines.push(
|
|
1511
|
-
|
|
1559
|
+
`${this.renderCursor(this.cursor === this.rows.length)} ${this.renderText(
|
|
1560
|
+
"Continue",
|
|
1561
|
+
this.cursor === this.rows.length ? "accent" : "text",
|
|
1562
|
+
)}`,
|
|
1512
1563
|
);
|
|
1513
1564
|
lines.push(
|
|
1514
|
-
|
|
1565
|
+
`${this.renderCursor(this.cursor === this.rows.length + 1)} ${this.renderText(
|
|
1566
|
+
"← Back",
|
|
1567
|
+
this.cursor === this.rows.length + 1 ? "accent" : "text",
|
|
1568
|
+
)}`,
|
|
1515
1569
|
);
|
|
1516
1570
|
lines.push("");
|
|
1517
1571
|
lines.push(
|
|
1518
1572
|
line(
|
|
1519
1573
|
"j/k scroll • enter model/save • e effort • i inherit • c custom • x export • r restore • ctrl+s save • esc back",
|
|
1574
|
+
"muted",
|
|
1520
1575
|
),
|
|
1521
1576
|
);
|
|
1522
1577
|
return lines;
|
|
@@ -1525,11 +1580,15 @@ class SddModelPanel implements OverlayComponent {
|
|
|
1525
1580
|
private renderModelPicker(width: number): string[] {
|
|
1526
1581
|
const lines: string[] = [];
|
|
1527
1582
|
const options = this.filteredModelOptions();
|
|
1528
|
-
const line = (text = "") =>
|
|
1529
|
-
|
|
1530
|
-
lines.push(
|
|
1583
|
+
const line = (text = "", tone?: PanelTone) =>
|
|
1584
|
+
this.renderLine(text, width, tone);
|
|
1585
|
+
lines.push(
|
|
1586
|
+
line(`Select model for ${sanitizeTerminalText(this.selectedRow)}`, "title"),
|
|
1587
|
+
);
|
|
1531
1588
|
lines.push("");
|
|
1532
|
-
lines.push(
|
|
1589
|
+
lines.push(
|
|
1590
|
+
`${this.renderText("◎", "accent")} ${this.renderText(this.query || "search...", "muted")}`,
|
|
1591
|
+
);
|
|
1533
1592
|
lines.push("");
|
|
1534
1593
|
const start = Math.max(
|
|
1535
1594
|
0,
|
|
@@ -1541,12 +1600,17 @@ class SddModelPanel implements OverlayComponent {
|
|
|
1541
1600
|
const end = Math.min(options.length, start + MODEL_LIST_MAX_VISIBLE_ROWS);
|
|
1542
1601
|
for (let i = start; i < end; i++) {
|
|
1543
1602
|
const focused = i === this.modelCursor;
|
|
1544
|
-
lines.push(
|
|
1603
|
+
lines.push(
|
|
1604
|
+
`${this.renderCursor(focused)} ${this.renderText(
|
|
1605
|
+
options[i] ?? "",
|
|
1606
|
+
focused ? "status" : "text",
|
|
1607
|
+
)}`,
|
|
1608
|
+
);
|
|
1545
1609
|
}
|
|
1546
|
-
if (options.length === 0) lines.push(line(" No matching models"));
|
|
1610
|
+
if (options.length === 0) lines.push(line(" No matching models", "muted"));
|
|
1547
1611
|
lines.push("");
|
|
1548
1612
|
lines.push(
|
|
1549
|
-
line("j/k: navigate • type: search • enter: select • esc: back"),
|
|
1613
|
+
line("j/k: navigate • type: search • enter: select • esc: back", "muted"),
|
|
1550
1614
|
);
|
|
1551
1615
|
return lines;
|
|
1552
1616
|
}
|
|
@@ -1580,16 +1644,23 @@ class SddModelPanel implements OverlayComponent {
|
|
|
1580
1644
|
|
|
1581
1645
|
private renderEffortPicker(width: number): string[] {
|
|
1582
1646
|
const lines: string[] = [];
|
|
1583
|
-
const line = (text = "") =>
|
|
1584
|
-
|
|
1585
|
-
lines.push(
|
|
1647
|
+
const line = (text = "", tone?: PanelTone) =>
|
|
1648
|
+
this.renderLine(text, width, tone);
|
|
1649
|
+
lines.push(
|
|
1650
|
+
line(`Select effort for ${sanitizeTerminalText(this.selectedRow)}`, "title"),
|
|
1651
|
+
);
|
|
1586
1652
|
lines.push("");
|
|
1587
1653
|
for (let i = 0; i < THINKING_OPTIONS.length; i++) {
|
|
1588
1654
|
const focused = i === this.effortCursor;
|
|
1589
|
-
lines.push(
|
|
1655
|
+
lines.push(
|
|
1656
|
+
`${this.renderCursor(focused)} ${this.renderText(
|
|
1657
|
+
THINKING_OPTIONS[i] ?? "",
|
|
1658
|
+
focused ? "status" : "text",
|
|
1659
|
+
)}`,
|
|
1660
|
+
);
|
|
1590
1661
|
}
|
|
1591
1662
|
lines.push("");
|
|
1592
|
-
lines.push(line("j/k: navigate • enter: select • esc: back"));
|
|
1663
|
+
lines.push(line("j/k: navigate • enter: select • esc: back", "muted"));
|
|
1593
1664
|
return lines;
|
|
1594
1665
|
}
|
|
1595
1666
|
|
|
@@ -1608,16 +1679,34 @@ class SddModelPanel implements OverlayComponent {
|
|
|
1608
1679
|
const effortLabel = efforts.every((value) => value === firstEffort)
|
|
1609
1680
|
? firstEffort
|
|
1610
1681
|
: "mixed";
|
|
1611
|
-
return `${sanitizeTerminalText(row).padEnd(20)} model
|
|
1682
|
+
return `${this.renderText(sanitizeTerminalText(row).padEnd(20), "text")} ${this.renderText("model=", "muted")}${this.renderText(modelLabel, "status")}${this.renderText(
|
|
1683
|
+
", effort=",
|
|
1684
|
+
"muted",
|
|
1685
|
+
)}${this.renderText(effortLabel, "status")}`;
|
|
1612
1686
|
}
|
|
1613
1687
|
|
|
1614
1688
|
private renderAgentLabel(row: string): string {
|
|
1615
1689
|
const model = this.draft[row]?.model ?? "inherit";
|
|
1616
1690
|
const effort = this.draft[row]?.thinking ?? "inherit";
|
|
1617
|
-
return `${sanitizeTerminalText(row).padEnd(20)}
|
|
1691
|
+
return `${this.renderText(sanitizeTerminalText(row).padEnd(20), "text")} ${this.renderText("model=", "muted")}${this.renderText(model, "status")}${this.renderText(
|
|
1692
|
+
", effort=",
|
|
1693
|
+
"muted",
|
|
1694
|
+
)}${this.renderText(effort, "status")}`;
|
|
1618
1695
|
}
|
|
1619
1696
|
}
|
|
1620
1697
|
|
|
1698
|
+
function renderSddModelPanelForTesting(
|
|
1699
|
+
initialConfig: AgentModelConfig,
|
|
1700
|
+
modelOptions: string[],
|
|
1701
|
+
agents: string[],
|
|
1702
|
+
width: number,
|
|
1703
|
+
theme?: Theme,
|
|
1704
|
+
): string[] {
|
|
1705
|
+
return new SddModelPanel(initialConfig, modelOptions, agents, () => {}, theme).render(
|
|
1706
|
+
width,
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1621
1710
|
async function showSddModelPanel(
|
|
1622
1711
|
ctx: ExtensionContext,
|
|
1623
1712
|
config: AgentModelConfig,
|
|
@@ -1625,8 +1714,8 @@ async function showSddModelPanel(
|
|
|
1625
1714
|
const modelOptions = await getPiModelOptions(ctx);
|
|
1626
1715
|
const agents = listDiscoverableAgents(ctx.cwd).map((agent) => agent.name);
|
|
1627
1716
|
return ctx.ui.custom<ModelPanelResult>(
|
|
1628
|
-
(_tui,
|
|
1629
|
-
new SddModelPanel(config, modelOptions, agents, done),
|
|
1717
|
+
(_tui, theme, _keybindings, done) =>
|
|
1718
|
+
new SddModelPanel(config, modelOptions, agents, done, theme),
|
|
1630
1719
|
{
|
|
1631
1720
|
overlay: true,
|
|
1632
1721
|
overlayOptions: {
|
|
@@ -1918,6 +2007,7 @@ export const __testing = {
|
|
|
1918
2007
|
buildGentlePrompt,
|
|
1919
2008
|
classifyReviewEvent,
|
|
1920
2009
|
parseNumstat,
|
|
2010
|
+
renderSddModelPanel: renderSddModelPanelForTesting,
|
|
1921
2011
|
};
|
|
1922
2012
|
|
|
1923
2013
|
export default function gentleAi(pi: ExtensionAPI): void {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const NON_OSC_ANSI_ESCAPE_PATTERN =
|
|
2
|
+
/\x1B\[[0-?]*[ -/]*[@-~]|\x1B[@-Z\\-_]|\x9B[0-?]*[ -/]*[@-~]/g;
|
|
3
|
+
const CONTROL_CHAR_PATTERN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g;
|
|
4
|
+
const OSC_ESCAPE_PATTERN = /(?:\x1B\]|\x9D)[\s\S]*?(?:\x07|\x1B\\|\x9C|$)/g;
|
|
5
|
+
|
|
6
|
+
export function stripAnsi(value: string): string {
|
|
7
|
+
return stripOsc(value).replace(NON_OSC_ANSI_ESCAPE_PATTERN, "");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function stripOsc(value: string): string {
|
|
11
|
+
return value.replace(OSC_ESCAPE_PATTERN, "");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function sanitizeTerminalText(value: string): string {
|
|
15
|
+
return stripAnsi(value).replace(CONTROL_CHAR_PATTERN, "");
|
|
16
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"skills/",
|
|
32
32
|
"scripts/",
|
|
33
33
|
"tests/",
|
|
34
|
+
"themes/",
|
|
34
35
|
"README.md"
|
|
35
36
|
],
|
|
36
37
|
"scripts": {
|
|
@@ -44,6 +45,9 @@
|
|
|
44
45
|
"extensions": [
|
|
45
46
|
"./extensions"
|
|
46
47
|
],
|
|
48
|
+
"themes": [
|
|
49
|
+
"./themes"
|
|
50
|
+
],
|
|
47
51
|
"prompts": [
|
|
48
52
|
"./prompts"
|
|
49
53
|
],
|
package/tests/gentle-ai.test.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
6
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import { __testing } from "../extensions/gentle-ai.ts";
|
|
8
|
+
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
7
9
|
|
|
8
10
|
function writeMarkdown(path: string, content: string): void {
|
|
9
11
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -83,3 +85,35 @@ test("discoverable model agents include installed Judgment Day agents", (t) => {
|
|
|
83
85
|
["jd-judge-a", "jd-judge-b", "jd-fix-agent"],
|
|
84
86
|
);
|
|
85
87
|
});
|
|
88
|
+
|
|
89
|
+
test("model panel render does not auto-apply the Gentle theme and sanitizes agent labels", () => {
|
|
90
|
+
const lines = __testing.renderSddModelPanel(
|
|
91
|
+
{},
|
|
92
|
+
["openai/gpt-5.5"],
|
|
93
|
+
["safe-agent\x1b[31m"],
|
|
94
|
+
72,
|
|
95
|
+
);
|
|
96
|
+
const rendered = lines.join("\n");
|
|
97
|
+
const plain = stripAnsi(rendered);
|
|
98
|
+
|
|
99
|
+
assert.doesNotMatch(rendered, /\x1b\[38;2;71;85;105m/);
|
|
100
|
+
assert.doesNotMatch(rendered, /\x1b\[38;2;125;211;252m/);
|
|
101
|
+
assert.match(plain, /Assign Models and Effort to Agents/);
|
|
102
|
+
assert.match(plain, /safe-agent\s+model=inherit, effort=inherit/);
|
|
103
|
+
assert.doesNotMatch(plain, /\[31m/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("model panel render uses the Pi-provided current theme when supplied", () => {
|
|
107
|
+
const currentTheme = {
|
|
108
|
+
fg(_color: string, text: string): string {
|
|
109
|
+
return `\x1b[35m${text}\x1b[39m`;
|
|
110
|
+
},
|
|
111
|
+
} as unknown as Theme;
|
|
112
|
+
|
|
113
|
+
const rendered = __testing
|
|
114
|
+
.renderSddModelPanel({}, ["openai/gpt-5.5"], ["safe-agent"], 72, currentTheme)
|
|
115
|
+
.join("\n");
|
|
116
|
+
|
|
117
|
+
assert.match(rendered, /\x1b\[35m/);
|
|
118
|
+
assert.match(stripAnsi(rendered), /Assign Models and Effort to Agents/);
|
|
119
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
|
|
9
|
+
const REQUIRED_THEME_COLOR_KEYS = [
|
|
10
|
+
"accent",
|
|
11
|
+
"border",
|
|
12
|
+
"borderAccent",
|
|
13
|
+
"borderMuted",
|
|
14
|
+
"success",
|
|
15
|
+
"error",
|
|
16
|
+
"warning",
|
|
17
|
+
"muted",
|
|
18
|
+
"dim",
|
|
19
|
+
"text",
|
|
20
|
+
"thinkingText",
|
|
21
|
+
"selectedBg",
|
|
22
|
+
"userMessageBg",
|
|
23
|
+
"userMessageText",
|
|
24
|
+
"customMessageBg",
|
|
25
|
+
"customMessageText",
|
|
26
|
+
"customMessageLabel",
|
|
27
|
+
"toolPendingBg",
|
|
28
|
+
"toolSuccessBg",
|
|
29
|
+
"toolErrorBg",
|
|
30
|
+
"toolTitle",
|
|
31
|
+
"toolOutput",
|
|
32
|
+
"mdHeading",
|
|
33
|
+
"mdLink",
|
|
34
|
+
"mdLinkUrl",
|
|
35
|
+
"mdCode",
|
|
36
|
+
"mdCodeBlock",
|
|
37
|
+
"mdCodeBlockBorder",
|
|
38
|
+
"mdQuote",
|
|
39
|
+
"mdQuoteBorder",
|
|
40
|
+
"mdHr",
|
|
41
|
+
"mdListBullet",
|
|
42
|
+
"toolDiffAdded",
|
|
43
|
+
"toolDiffRemoved",
|
|
44
|
+
"toolDiffContext",
|
|
45
|
+
"syntaxComment",
|
|
46
|
+
"syntaxKeyword",
|
|
47
|
+
"syntaxFunction",
|
|
48
|
+
"syntaxVariable",
|
|
49
|
+
"syntaxString",
|
|
50
|
+
"syntaxNumber",
|
|
51
|
+
"syntaxType",
|
|
52
|
+
"syntaxOperator",
|
|
53
|
+
"syntaxPunctuation",
|
|
54
|
+
"thinkingOff",
|
|
55
|
+
"thinkingMinimal",
|
|
56
|
+
"thinkingLow",
|
|
57
|
+
"thinkingMedium",
|
|
58
|
+
"thinkingHigh",
|
|
59
|
+
"thinkingXhigh",
|
|
60
|
+
"bashMode",
|
|
61
|
+
] as const;
|
|
62
|
+
|
|
63
|
+
interface PackageJsonPiManifest {
|
|
64
|
+
theme?: string;
|
|
65
|
+
themes?: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface PackageJson {
|
|
69
|
+
files?: string[];
|
|
70
|
+
pi?: PackageJsonPiManifest;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface GentleThemeJson {
|
|
74
|
+
name?: string;
|
|
75
|
+
vars?: Record<string, unknown>;
|
|
76
|
+
colors?: Record<string, unknown>;
|
|
77
|
+
export?: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function resolveThemeColor(theme: GentleThemeJson, colorKey: string): unknown {
|
|
81
|
+
const colorRef = theme.colors?.[colorKey];
|
|
82
|
+
|
|
83
|
+
if (typeof colorRef !== "string") {
|
|
84
|
+
return colorRef;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return theme.vars?.[colorRef] ?? colorRef;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function assertResolvedThemeColor(
|
|
91
|
+
theme: GentleThemeJson,
|
|
92
|
+
colorKey: string,
|
|
93
|
+
expected: string,
|
|
94
|
+
): void {
|
|
95
|
+
assert.equal(resolveThemeColor(theme, colorKey), expected, colorKey);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function readJson<T>(path: string): T {
|
|
99
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
test("package manifest exposes bundled themes to Pi discovery", () => {
|
|
103
|
+
const packageJson = readJson<PackageJson>(join(PACKAGE_ROOT, "package.json"));
|
|
104
|
+
|
|
105
|
+
assert.ok(
|
|
106
|
+
packageJson.pi?.themes?.includes("./themes"),
|
|
107
|
+
"package.json must expose ./themes so Pi can list bundled themes in Settings",
|
|
108
|
+
);
|
|
109
|
+
assert.ok(
|
|
110
|
+
packageJson.files?.includes("themes/"),
|
|
111
|
+
"package files must include themes/ so the bundled theme ships in npm packages",
|
|
112
|
+
);
|
|
113
|
+
assert.equal(
|
|
114
|
+
packageJson.pi?.theme,
|
|
115
|
+
undefined,
|
|
116
|
+
"package manifest must not auto-apply the bundled theme",
|
|
117
|
+
);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("bundled Pi theme is named exactly Gentle and defines all required colors", () => {
|
|
121
|
+
const theme = readJson<GentleThemeJson>(
|
|
122
|
+
join(PACKAGE_ROOT, "themes", "Gentle.json"),
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
assert.equal(theme.name, "Gentle");
|
|
126
|
+
const colors = theme.colors ?? {};
|
|
127
|
+
assert.deepEqual(
|
|
128
|
+
REQUIRED_THEME_COLOR_KEYS.filter((key) => !(key in colors)),
|
|
129
|
+
[],
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("bundled Pi theme maps roles to the subtle OpenCode gentleman theme", () => {
|
|
134
|
+
const theme = readJson<GentleThemeJson>(
|
|
135
|
+
join(PACKAGE_ROOT, "themes", "Gentle.json"),
|
|
136
|
+
);
|
|
137
|
+
const vars = theme.vars ?? {};
|
|
138
|
+
const colors = theme.colors ?? {};
|
|
139
|
+
|
|
140
|
+
assert.equal(vars.bg, "#06080f", "Pi background should match OpenCode panel background");
|
|
141
|
+
assert.equal(vars.bgPanel, "#06080f", "large panel surfaces should match OpenCode panels");
|
|
142
|
+
assert.equal(vars.bgElement, "#06080f", "message surfaces should match OpenCode elements");
|
|
143
|
+
assert.equal(vars.bgSubtle, "#0d0f14", "secondary surfaces should use OpenCode diff context background");
|
|
144
|
+
assert.equal(vars.text, "#F3F6F9", "text should match OpenCode foreground");
|
|
145
|
+
assert.equal(vars.muted, "#5C6170", "muted text should match OpenCode textMuted");
|
|
146
|
+
assert.equal(vars.border, "#313342", "border should match OpenCode border");
|
|
147
|
+
assert.equal(vars.borderSubtle, "#232A40", "subtle borders should match OpenCode borderSubtle");
|
|
148
|
+
assert.equal(vars.accent, "#E0C15A", "source accent should remain available");
|
|
149
|
+
assert.equal(vars.selection, "#232A40", "selection should use OpenCode borderSubtle");
|
|
150
|
+
assert.equal(vars.red, "#CB7C94", "error accent should use blur red");
|
|
151
|
+
assert.equal(vars.green, "#B7CC85", "success accent should use blur green");
|
|
152
|
+
assert.equal(vars.blue, "#7FB4CA", "primary roles should use OpenCode primary blue");
|
|
153
|
+
assert.equal(vars.secondary, "#A3B5D6", "secondary roles should match OpenCode secondary");
|
|
154
|
+
assert.equal(vars.heading, "#B5B2D0", "markdown headings should match OpenCode heading");
|
|
155
|
+
assert.equal(vars.warning, "#DEBA87", "warning should match OpenCode warning");
|
|
156
|
+
assert.equal(
|
|
157
|
+
vars.toolSuccessBg,
|
|
158
|
+
"#1a2e1a",
|
|
159
|
+
"success background should match OpenCode diff added background",
|
|
160
|
+
);
|
|
161
|
+
assert.equal(
|
|
162
|
+
vars.toolPendingBg,
|
|
163
|
+
"#0d0f14",
|
|
164
|
+
"pending background should match OpenCode diff context background",
|
|
165
|
+
);
|
|
166
|
+
assert.equal(
|
|
167
|
+
vars.toolErrorBg,
|
|
168
|
+
"#2e1a1a",
|
|
169
|
+
"error background should match OpenCode diff removed background",
|
|
170
|
+
);
|
|
171
|
+
assert.equal(
|
|
172
|
+
vars.infoBg,
|
|
173
|
+
"#0d0f14",
|
|
174
|
+
"info background should stay as subtle as OpenCode diff context background",
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
assert.equal(colors.accent, "blue");
|
|
178
|
+
assert.equal(colors.selectedBg, "selection");
|
|
179
|
+
assert.equal(colors.borderAccent, "blue");
|
|
180
|
+
assert.equal(colors.userMessageBg, "bgElement");
|
|
181
|
+
assert.equal(colors.customMessageBg, "bgSubtle");
|
|
182
|
+
assert.notEqual(resolveThemeColor(theme, "borderAccent"), "#E0C15A");
|
|
183
|
+
assert.equal(colors.thinkingText, "muted");
|
|
184
|
+
assert.equal(colors.bashMode, "blue");
|
|
185
|
+
assert.equal(colors.toolPendingBg, "toolPendingBg");
|
|
186
|
+
assert.equal(colors.toolSuccessBg, "toolSuccessBg");
|
|
187
|
+
assert.equal(colors.toolErrorBg, "toolErrorBg");
|
|
188
|
+
assert.equal(colors.mdHeading, "heading");
|
|
189
|
+
assert.equal(colors.mdLink, "blue");
|
|
190
|
+
assert.equal(colors.mdCode, "green");
|
|
191
|
+
assert.equal(colors.mdQuote, "warning");
|
|
192
|
+
assert.equal(colors.mdListBullet, "blue");
|
|
193
|
+
assert.equal(colors.toolTitle, "blue");
|
|
194
|
+
assert.equal(colors.thinkingHigh, "blue");
|
|
195
|
+
assert.equal(colors.thinkingXhigh, "secondary");
|
|
196
|
+
assert.equal(colors.toolOutput, "text");
|
|
197
|
+
assert.equal(theme.export?.pageBg, "bg");
|
|
198
|
+
assert.equal(theme.export?.cardBg, "bgElement");
|
|
199
|
+
|
|
200
|
+
assertResolvedThemeColor(theme, "accent", "#7FB4CA");
|
|
201
|
+
assertResolvedThemeColor(theme, "border", "#313342");
|
|
202
|
+
assertResolvedThemeColor(theme, "borderMuted", "#232A40");
|
|
203
|
+
assertResolvedThemeColor(theme, "borderAccent", "#7FB4CA");
|
|
204
|
+
assertResolvedThemeColor(theme, "selectedBg", "#232A40");
|
|
205
|
+
assertResolvedThemeColor(theme, "thinkingText", "#5C6170");
|
|
206
|
+
assertResolvedThemeColor(theme, "bashMode", "#7FB4CA");
|
|
207
|
+
assertResolvedThemeColor(theme, "toolSuccessBg", "#1a2e1a");
|
|
208
|
+
assertResolvedThemeColor(theme, "toolErrorBg", "#2e1a1a");
|
|
209
|
+
assertResolvedThemeColor(theme, "toolPendingBg", "#0d0f14");
|
|
210
|
+
assertResolvedThemeColor(theme, "mdHeading", "#B5B2D0");
|
|
211
|
+
assertResolvedThemeColor(theme, "mdLink", "#7FB4CA");
|
|
212
|
+
assertResolvedThemeColor(theme, "mdCode", "#B7CC85");
|
|
213
|
+
assertResolvedThemeColor(theme, "mdQuote", "#DEBA87");
|
|
214
|
+
assertResolvedThemeColor(theme, "syntaxComment", "#8394A3");
|
|
215
|
+
assertResolvedThemeColor(theme, "syntaxKeyword", "#C99AD6");
|
|
216
|
+
assertResolvedThemeColor(theme, "syntaxFunction", "#B99BF2");
|
|
217
|
+
assertResolvedThemeColor(theme, "syntaxString", "#DFBD76");
|
|
218
|
+
assertResolvedThemeColor(theme, "syntaxNumber", "#A4DAA7");
|
|
219
|
+
assertResolvedThemeColor(theme, "syntaxType", "#8FB8DD");
|
|
220
|
+
assertResolvedThemeColor(theme, "syntaxPunctuation", "#96A2B0");
|
|
221
|
+
});
|
|
@@ -7,6 +7,7 @@ import { dirname, join } from "node:path";
|
|
|
7
7
|
import { discoverAndLoadExtensions } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { matchesKey } from "@earendil-works/pi-tui";
|
|
9
9
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
10
|
+
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
10
11
|
|
|
11
12
|
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
12
13
|
const EXTENSIONS = [
|
|
@@ -823,8 +824,9 @@ async function run() {
|
|
|
823
824
|
ctx.ui.custom = (factory) => {
|
|
824
825
|
const panel = factory(null, null, null, () => undefined);
|
|
825
826
|
const initialLines = panel.render(120);
|
|
827
|
+
const plainInitialLines = initialLines.map(stripAnsi);
|
|
826
828
|
assert.ok(
|
|
827
|
-
|
|
829
|
+
plainInitialLines[0].startsWith("╭") && plainInitialLines.at(-1).startsWith("╰"),
|
|
828
830
|
"model panel should render inside a bordered card",
|
|
829
831
|
);
|
|
830
832
|
assert.ok(
|
|
@@ -832,36 +834,43 @@ async function run() {
|
|
|
832
834
|
"long model agent list should fit within a 24-row terminal 85% overlay budget",
|
|
833
835
|
);
|
|
834
836
|
assert.ok(
|
|
835
|
-
|
|
837
|
+
plainInitialLines.some((line) => /↓ \d+ more agent\(s\)/.test(line)),
|
|
836
838
|
"long model agent list should render a down-scroll indicator",
|
|
837
839
|
);
|
|
838
840
|
assert.ok(
|
|
839
|
-
|
|
841
|
+
plainInitialLines.some((line) => line.includes("Continue")),
|
|
840
842
|
"long model agent list should keep Continue visible",
|
|
841
843
|
);
|
|
842
844
|
assert.doesNotMatch(
|
|
843
845
|
initialLines.join("\n"),
|
|
844
|
-
|
|
845
|
-
"model panel must strip terminal control sequences from agent labels",
|
|
846
|
+
/\u001b\]|\u0007/,
|
|
847
|
+
"model panel must strip unsafe terminal control sequences from agent labels",
|
|
848
|
+
);
|
|
849
|
+
assert.doesNotMatch(
|
|
850
|
+
plainInitialLines.join("\n"),
|
|
851
|
+
/\]52|\[31m/,
|
|
852
|
+
"model panel must strip user-provided terminal escapes from labels",
|
|
846
853
|
);
|
|
847
854
|
for (let i = 0; i < 20; i++) panel.handleInput("j");
|
|
848
855
|
const scrolledLines = panel.render(120);
|
|
856
|
+
const plainScrolledLines = scrolledLines.map(stripAnsi);
|
|
849
857
|
assert.ok(
|
|
850
858
|
scrolledLines.length <= 20,
|
|
851
859
|
"scrolled model agent list should stay within the overlay height budget",
|
|
852
860
|
);
|
|
853
861
|
assert.ok(
|
|
854
|
-
|
|
862
|
+
plainScrolledLines.some((line) => /↑ \d+ more agent\(s\)/.test(line)),
|
|
855
863
|
"long model agent list should render an up-scroll indicator after navigation",
|
|
856
864
|
);
|
|
857
865
|
panel.handleInput("G");
|
|
858
866
|
const bottomLines = panel.render(120);
|
|
867
|
+
const plainBottomLines = bottomLines.map(stripAnsi);
|
|
859
868
|
assert.ok(
|
|
860
869
|
bottomLines.length <= 20,
|
|
861
870
|
"bottom model agent list should stay within the overlay height budget",
|
|
862
871
|
);
|
|
863
872
|
assert.ok(
|
|
864
|
-
|
|
873
|
+
plainBottomLines.some((line) => line.includes("▸ ← Back")),
|
|
865
874
|
"G should jump to the Back action",
|
|
866
875
|
);
|
|
867
876
|
return Promise.resolve({ type: "cancel" });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { sanitizeTerminalText, stripAnsi } from "../lib/terminal-theme.ts";
|
|
4
|
+
|
|
5
|
+
test("sanitizeTerminalText removes user-controlled ANSI and control characters", () => {
|
|
6
|
+
assert.equal(
|
|
7
|
+
sanitizeTerminalText("safe\x1b[31mred\x1b[0m\x1b]52;c;Zm9v\u0007text"),
|
|
8
|
+
"saferedtext",
|
|
9
|
+
);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("sanitizeTerminalText removes unterminated OSC payloads", () => {
|
|
13
|
+
assert.equal(sanitizeTerminalText("safe\x1b]52;c;Zm9vtext"), "safe");
|
|
14
|
+
assert.equal(sanitizeTerminalText("safe\x9d52;c;Zm9vtext"), "safe");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("stripAnsi removes OSC payloads and ANSI styling", () => {
|
|
18
|
+
assert.equal(
|
|
19
|
+
stripAnsi("safe \x1b]52;c;Zm9v\u0007\x1b[31mred\x1b[0m"),
|
|
20
|
+
"safe red",
|
|
21
|
+
);
|
|
22
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/earendil-works/pi-mono/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
|
|
3
|
+
"name": "Gentle",
|
|
4
|
+
"vars": {
|
|
5
|
+
"bg": "#06080f",
|
|
6
|
+
"bgPanel": "#06080f",
|
|
7
|
+
"bgElement": "#06080f",
|
|
8
|
+
"bgSubtle": "#0d0f14",
|
|
9
|
+
"surfaceLine": "#0d0f14",
|
|
10
|
+
"border": "#313342",
|
|
11
|
+
"borderSubtle": "#232A40",
|
|
12
|
+
"text": "#F3F6F9",
|
|
13
|
+
"muted": "#5C6170",
|
|
14
|
+
"dim": "#5C6170",
|
|
15
|
+
"disabled": "#5C6170",
|
|
16
|
+
"accent": "#E0C15A",
|
|
17
|
+
"blue": "#7FB4CA",
|
|
18
|
+
"secondary": "#A3B5D6",
|
|
19
|
+
"heading": "#B5B2D0",
|
|
20
|
+
"syntaxComment": "#8394A3",
|
|
21
|
+
"syntaxKeyword": "#C99AD6",
|
|
22
|
+
"syntaxFunction": "#B99BF2",
|
|
23
|
+
"syntaxString": "#DFBD76",
|
|
24
|
+
"syntaxNumber": "#A4DAA7",
|
|
25
|
+
"syntaxType": "#8FB8DD",
|
|
26
|
+
"syntaxPunctuation": "#96A2B0",
|
|
27
|
+
"green": "#B7CC85",
|
|
28
|
+
"warning": "#DEBA87",
|
|
29
|
+
"red": "#CB7C94",
|
|
30
|
+
"brightBlack": "#8394A3",
|
|
31
|
+
"brightGreen": "#D1E8A9",
|
|
32
|
+
"brightYellow": "#DEBA87",
|
|
33
|
+
"brightPurple": "#B99BF2",
|
|
34
|
+
"brightMagenta": "#C99AD6",
|
|
35
|
+
"brightBlue": "#8FB8DD",
|
|
36
|
+
"selection": "#232A40",
|
|
37
|
+
"toolSuccessBg": "#1a2e1a",
|
|
38
|
+
"toolPendingBg": "#0d0f14",
|
|
39
|
+
"toolErrorBg": "#2e1a1a",
|
|
40
|
+
"infoBg": "#0d0f14"
|
|
41
|
+
},
|
|
42
|
+
"colors": {
|
|
43
|
+
"accent": "blue",
|
|
44
|
+
"border": "border",
|
|
45
|
+
"borderAccent": "blue",
|
|
46
|
+
"borderMuted": "borderSubtle",
|
|
47
|
+
"success": "green",
|
|
48
|
+
"error": "red",
|
|
49
|
+
"warning": "warning",
|
|
50
|
+
"muted": "muted",
|
|
51
|
+
"dim": "dim",
|
|
52
|
+
"text": "text",
|
|
53
|
+
"thinkingText": "muted",
|
|
54
|
+
"selectedBg": "selection",
|
|
55
|
+
"userMessageBg": "bgElement",
|
|
56
|
+
"userMessageText": "text",
|
|
57
|
+
"customMessageBg": "bgSubtle",
|
|
58
|
+
"customMessageText": "text",
|
|
59
|
+
"customMessageLabel": "blue",
|
|
60
|
+
"toolPendingBg": "toolPendingBg",
|
|
61
|
+
"toolSuccessBg": "toolSuccessBg",
|
|
62
|
+
"toolErrorBg": "toolErrorBg",
|
|
63
|
+
"toolTitle": "blue",
|
|
64
|
+
"toolOutput": "text",
|
|
65
|
+
"mdHeading": "heading",
|
|
66
|
+
"mdLink": "blue",
|
|
67
|
+
"mdLinkUrl": "muted",
|
|
68
|
+
"mdCode": "green",
|
|
69
|
+
"mdCodeBlock": "text",
|
|
70
|
+
"mdCodeBlockBorder": "borderSubtle",
|
|
71
|
+
"mdQuote": "warning",
|
|
72
|
+
"mdQuoteBorder": "borderSubtle",
|
|
73
|
+
"mdHr": "muted",
|
|
74
|
+
"mdListBullet": "blue",
|
|
75
|
+
"toolDiffAdded": "green",
|
|
76
|
+
"toolDiffRemoved": "red",
|
|
77
|
+
"toolDiffContext": "muted",
|
|
78
|
+
"syntaxComment": "syntaxComment",
|
|
79
|
+
"syntaxKeyword": "syntaxKeyword",
|
|
80
|
+
"syntaxFunction": "syntaxFunction",
|
|
81
|
+
"syntaxVariable": "text",
|
|
82
|
+
"syntaxString": "syntaxString",
|
|
83
|
+
"syntaxNumber": "syntaxNumber",
|
|
84
|
+
"syntaxType": "syntaxType",
|
|
85
|
+
"syntaxOperator": "warning",
|
|
86
|
+
"syntaxPunctuation": "syntaxPunctuation",
|
|
87
|
+
"thinkingOff": "borderSubtle",
|
|
88
|
+
"thinkingMinimal": "dim",
|
|
89
|
+
"thinkingLow": "muted",
|
|
90
|
+
"thinkingMedium": "blue",
|
|
91
|
+
"thinkingHigh": "blue",
|
|
92
|
+
"thinkingXhigh": "secondary",
|
|
93
|
+
"bashMode": "blue"
|
|
94
|
+
},
|
|
95
|
+
"export": {
|
|
96
|
+
"pageBg": "bg",
|
|
97
|
+
"cardBg": "bgElement",
|
|
98
|
+
"infoBg": "infoBg"
|
|
99
|
+
}
|
|
100
|
+
}
|