pi-studio 0.6.0 → 0.6.1

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/index.ts CHANGED
@@ -593,6 +593,8 @@ interface StudioPalette {
593
593
  interface StudioThemeStyle {
594
594
  mode: StudioThemeMode;
595
595
  palette: StudioPalette;
596
+ accentContrast?: string;
597
+ errorContrast?: string;
596
598
  }
597
599
 
598
600
  const DARK_STUDIO_PALETTE: StudioPalette = {
@@ -673,9 +675,39 @@ const LIGHT_STUDIO_PALETTE: StudioPalette = {
673
675
  syntaxPunctuation: "#000000",
674
676
  };
675
677
 
678
+ function inferThemeModeFromName(name: string): StudioThemeMode | undefined {
679
+ const lower = name.toLowerCase();
680
+ if (/\b(light|dawn|day|latte)\b/.test(lower) || lower.includes("-light")) return "light";
681
+ if (/\b(dark|night|moon|mocha)\b/.test(lower) || lower.includes("-dark")) return "dark";
682
+ return undefined;
683
+ }
684
+
685
+ function inferThemeModeFromColorCandidates(...colors: Array<string | undefined>): StudioThemeMode | undefined {
686
+ for (const color of colors) {
687
+ const inferred = inferThemeModeFromColor(color);
688
+ if (inferred) return inferred;
689
+ }
690
+ return undefined;
691
+ }
692
+
676
693
  function getStudioThemeMode(theme?: Theme): StudioThemeMode {
677
- const name = (theme?.name ?? "").toLowerCase();
678
- return name.includes("light") ? "light" : "dark";
694
+ const exported = readThemeExportPalette(theme);
695
+ const inferredFromExport = inferThemeModeFromColorCandidates(exported?.pageBg, exported?.cardBg);
696
+ if (inferredFromExport) return inferredFromExport;
697
+
698
+ const inferredFromSurface = inferThemeModeFromColorCandidates(
699
+ inferThemeSurfaceColor(theme, "page"),
700
+ inferThemeSurfaceColor(theme, "card"),
701
+ readThemeColorToken(theme, "userMessageBg"),
702
+ readThemeColorToken(theme, "customMessageBg"),
703
+ readThemeColorToken(theme, "toolPendingBg"),
704
+ );
705
+ if (inferredFromSurface) return inferredFromSurface;
706
+
707
+ const inferredFromName = inferThemeModeFromName(theme?.name ?? "");
708
+ if (inferredFromName) return inferredFromName;
709
+
710
+ return "dark";
679
711
  }
680
712
 
681
713
  function toHexByte(value: number): string {
@@ -809,6 +841,29 @@ function blendColors(a: string, b: string, t: number): string {
809
841
  );
810
842
  }
811
843
 
844
+ function wcagRelativeLuminance(color: string): number {
845
+ const rgb = hexToRgb(color);
846
+ if (!rgb) return 0;
847
+ const linear = [rgb.r, rgb.g, rgb.b].map((channel) => {
848
+ const value = channel / 255;
849
+ return value <= 0.03928 ? value / 12.92 : ((value + 0.055) / 1.055) ** 2.4;
850
+ });
851
+ return 0.2126 * linear[0]! + 0.7152 * linear[1]! + 0.0722 * linear[2]!;
852
+ }
853
+
854
+ function contrastRatio(a: string, b: string): number {
855
+ const lumA = wcagRelativeLuminance(a);
856
+ const lumB = wcagRelativeLuminance(b);
857
+ const lighter = Math.max(lumA, lumB);
858
+ const darker = Math.min(lumA, lumB);
859
+ return (lighter + 0.05) / (darker + 0.05);
860
+ }
861
+
862
+ function readableTextOn(background: string, darkText = "#0e1616", lightText = "#ffffff"): string {
863
+ if (!hexToRgb(background)) return lightText;
864
+ return contrastRatio(background, darkText) >= contrastRatio(background, lightText) ? darkText : lightText;
865
+ }
866
+
812
867
  function deriveCanvasColors(
813
868
  baseColor: string,
814
869
  mode: StudioThemeMode,
@@ -848,7 +903,17 @@ interface ThemeExportPalette {
848
903
  infoBg?: string;
849
904
  }
850
905
 
851
- const themeExportPaletteCache = new Map<string, ThemeExportPalette | null>();
906
+ interface ThemeSourceJson {
907
+ name?: string;
908
+ vars?: Record<string, string | number>;
909
+ colors?: Record<string, string | number>;
910
+ export?: { pageBg?: string | number; cardBg?: string | number; infoBg?: string | number };
911
+ }
912
+
913
+ const themeSourceJsonCache = new Map<string, { mtimeMs: number; json: ThemeSourceJson | null }>();
914
+
915
+ const DEFAULT_UI_FONT_STACK = "-apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif";
916
+ const DEFAULT_PROSE_FONT_STACK = DEFAULT_UI_FONT_STACK;
852
917
 
853
918
  const DEFAULT_MONO_FONT_FAMILIES = [
854
919
  "ui-monospace",
@@ -1049,6 +1114,14 @@ function getStudioMonoFontStack(): string {
1049
1114
  return cachedStudioMonoFontStack;
1050
1115
  }
1051
1116
 
1117
+ function getStudioUiFontStack(): string {
1118
+ return sanitizeCssValue(process.env.PI_STUDIO_FONT_UI ?? "") || DEFAULT_UI_FONT_STACK;
1119
+ }
1120
+
1121
+ function getStudioProseFontStack(): string {
1122
+ return sanitizeCssValue(process.env.PI_STUDIO_FONT_PROSE ?? "") || DEFAULT_PROSE_FONT_STACK;
1123
+ }
1124
+
1052
1125
  function resolveThemeExportValue(
1053
1126
  value: string | number | undefined,
1054
1127
  vars: Record<string, string | number>,
@@ -1071,37 +1144,109 @@ function resolveThemeExportValue(
1071
1144
  return resolveThemeExportValue(referenced, vars, seen) ?? token;
1072
1145
  }
1073
1146
 
1074
- function readThemeExportPalette(theme?: Theme): ThemeExportPalette | undefined {
1147
+ function isCssColorValue(value: string | undefined): value is string {
1148
+ if (!value) return false;
1149
+ const trimmed = value.trim();
1150
+ return /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(trimmed) || /^rgba?\(/i.test(trimmed);
1151
+ }
1152
+
1153
+ function normalizeResolvedThemeColor(value: string | undefined): string | undefined {
1154
+ if (!isCssColorValue(value)) return undefined;
1155
+ return value.trim();
1156
+ }
1157
+
1158
+ function readThemeSourceJson(theme?: Theme): ThemeSourceJson | undefined {
1075
1159
  const sourcePath = theme?.sourcePath?.trim();
1076
1160
  if (!sourcePath) return undefined;
1077
1161
 
1078
- if (themeExportPaletteCache.has(sourcePath)) {
1079
- const cached = themeExportPaletteCache.get(sourcePath);
1080
- return cached ?? undefined;
1081
- }
1082
-
1083
1162
  try {
1084
- const raw = readFileSync(sourcePath, "utf-8");
1085
- const parsed = JSON.parse(raw) as {
1086
- export?: { pageBg?: string | number; cardBg?: string | number; infoBg?: string | number };
1087
- vars?: Record<string, string | number>;
1088
- };
1089
- const vars = parsed.vars ?? {};
1090
- const exportSection = parsed.export ?? {};
1091
- const resolved: ThemeExportPalette = {
1092
- pageBg: resolveThemeExportValue(exportSection.pageBg, vars),
1093
- cardBg: resolveThemeExportValue(exportSection.cardBg, vars),
1094
- infoBg: resolveThemeExportValue(exportSection.infoBg, vars),
1095
- };
1163
+ const mtimeMs = statSync(sourcePath).mtimeMs;
1164
+ const cached = themeSourceJsonCache.get(sourcePath);
1165
+ if (cached && cached.mtimeMs === mtimeMs) return cached.json ?? undefined;
1096
1166
 
1097
- themeExportPaletteCache.set(sourcePath, resolved);
1098
- return resolved;
1167
+ const raw = readFileSync(sourcePath, "utf-8");
1168
+ const parsed = JSON.parse(raw) as ThemeSourceJson;
1169
+ themeSourceJsonCache.set(sourcePath, { mtimeMs, json: parsed });
1170
+ return parsed;
1099
1171
  } catch {
1100
- themeExportPaletteCache.set(sourcePath, null);
1172
+ themeSourceJsonCache.set(sourcePath, { mtimeMs: -1, json: null });
1101
1173
  return undefined;
1102
1174
  }
1103
1175
  }
1104
1176
 
1177
+ function resolveThemeJsonValue(
1178
+ value: string | number | undefined,
1179
+ vars: Record<string, string | number>,
1180
+ ): string | undefined {
1181
+ return normalizeResolvedThemeColor(resolveThemeExportValue(value, vars));
1182
+ }
1183
+
1184
+ function readThemeExportPalette(theme?: Theme): ThemeExportPalette | undefined {
1185
+ const parsed = readThemeSourceJson(theme);
1186
+ if (!parsed) return undefined;
1187
+ const vars = parsed.vars ?? {};
1188
+ const exportSection = parsed.export ?? {};
1189
+ const resolved: ThemeExportPalette = {
1190
+ pageBg: resolveThemeJsonValue(exportSection.pageBg, vars),
1191
+ cardBg: resolveThemeJsonValue(exportSection.cardBg, vars),
1192
+ infoBg: resolveThemeJsonValue(exportSection.infoBg, vars),
1193
+ };
1194
+ return resolved.pageBg || resolved.cardBg || resolved.infoBg ? resolved : undefined;
1195
+ }
1196
+
1197
+ function readThemeColorToken(theme: Theme | undefined, token: string): string | undefined {
1198
+ const parsed = readThemeSourceJson(theme);
1199
+ if (!parsed) return undefined;
1200
+ return resolveThemeJsonValue(parsed.colors?.[token], parsed.vars ?? {});
1201
+ }
1202
+
1203
+ function readThemeVarColor(theme: Theme | undefined, keys: string[]): string | undefined {
1204
+ const parsed = readThemeSourceJson(theme);
1205
+ if (!parsed) return undefined;
1206
+ const vars = parsed.vars ?? {};
1207
+ for (const key of keys) {
1208
+ const color = resolveThemeJsonValue(vars[key], vars);
1209
+ if (color) return color;
1210
+ }
1211
+ return undefined;
1212
+ }
1213
+
1214
+ function readThemeAnyColor(theme: Theme | undefined, keys: string[]): string | undefined {
1215
+ const parsed = readThemeSourceJson(theme);
1216
+ if (!parsed) return undefined;
1217
+ const vars = parsed.vars ?? {};
1218
+ for (const key of keys) {
1219
+ const color = resolveThemeJsonValue(parsed.colors?.[key], vars);
1220
+ if (color) return color;
1221
+ }
1222
+ return undefined;
1223
+ }
1224
+
1225
+ function inferThemeModeFromColor(color: string | undefined): StudioThemeMode | undefined {
1226
+ if (!color || !hexToRgb(color)) return undefined;
1227
+ return relativeLuminance(color) >= 0.58 ? "light" : "dark";
1228
+ }
1229
+
1230
+ function inferThemeTextColor(theme: Theme | undefined, mode: StudioThemeMode): string | undefined {
1231
+ return readThemeAnyColor(theme, ["text", "userMessageText", "customMessageText", "mdCodeBlock"])
1232
+ ?? readThemeVarColor(
1233
+ theme,
1234
+ mode === "light"
1235
+ ? ["text", "fg", "foreground", "textDark1", "fg0", "fg1", "nord0"]
1236
+ : ["text", "fg", "foreground", "text", "fg0", "fg1", "subtext1", "subtext0", "nord4", "gray3"],
1237
+ );
1238
+ }
1239
+
1240
+ function inferThemeSurfaceColor(theme: Theme | undefined, role: "page" | "card" | "panel2"): string | undefined {
1241
+ if (role === "page") {
1242
+ return readThemeVarColor(theme, ["pageBg", "bg", "base", "background", "mantle", "bg_dark", "bg0", "nord0"]);
1243
+ }
1244
+ if (role === "card") {
1245
+ return readThemeVarColor(theme, ["cardBg", "surface", "base", "bg", "bg1", "nord1"]);
1246
+ }
1247
+ return readThemeVarColor(theme, ["infoBg", "surfaceAlt", "surface0", "overlay", "bg_hl", "bg2", "nord2"]);
1248
+ }
1249
+
1105
1250
  function getStudioThemeStyle(theme?: Theme): StudioThemeStyle {
1106
1251
  const mode = getStudioThemeMode(theme);
1107
1252
  const fallback = mode === "light" ? LIGHT_STUDIO_PALETTE : DARK_STUDIO_PALETTE;
@@ -1116,36 +1261,49 @@ function getStudioThemeStyle(theme?: Theme): StudioThemeStyle {
1116
1261
  const accent =
1117
1262
  safeThemeColor(() => theme.getFgAnsi("mdLink"))
1118
1263
  ?? safeThemeColor(() => theme.getFgAnsi("accent"))
1264
+ ?? readThemeColorToken(theme, "mdLink")
1265
+ ?? readThemeColorToken(theme, "accent")
1119
1266
  ?? fallback.accent;
1120
- const warn = safeThemeColor(() => theme.getFgAnsi("warning")) ?? fallback.warn;
1121
- const error = safeThemeColor(() => theme.getFgAnsi("error")) ?? fallback.error;
1122
- const ok = safeThemeColor(() => theme.getFgAnsi("success")) ?? fallback.ok;
1267
+ const warn = safeThemeColor(() => theme.getFgAnsi("warning")) ?? readThemeColorToken(theme, "warning") ?? fallback.warn;
1268
+ const error = safeThemeColor(() => theme.getFgAnsi("error")) ?? readThemeColorToken(theme, "error") ?? fallback.error;
1269
+ const ok = safeThemeColor(() => theme.getFgAnsi("success")) ?? readThemeColorToken(theme, "success") ?? fallback.ok;
1270
+ const text = safeThemeColor(() => theme.getFgAnsi("text")) ?? inferThemeTextColor(theme, mode) ?? fallback.text;
1123
1271
  const exported = readThemeExportPalette(theme);
1124
1272
 
1125
1273
  const surfaceBase =
1126
1274
  safeThemeColor(() => theme.getBgAnsi("userMessageBg"))
1127
- ?? safeThemeColor(() => theme.getBgAnsi("customMessageBg"));
1275
+ ?? safeThemeColor(() => theme.getBgAnsi("customMessageBg"))
1276
+ ?? readThemeColorToken(theme, "userMessageBg")
1277
+ ?? readThemeColorToken(theme, "customMessageBg");
1128
1278
  const derived = surfaceBase ? deriveCanvasColors(surfaceBase, mode) : undefined;
1279
+ const themePageBg = inferThemeSurfaceColor(theme, "page");
1280
+ const themeCardBg = inferThemeSurfaceColor(theme, "card");
1281
+ const themePanel2 = inferThemeSurfaceColor(theme, "panel2");
1129
1282
 
1130
1283
  const palette: StudioPalette = {
1131
1284
  bg:
1132
1285
  exported?.pageBg
1286
+ ?? themePageBg
1133
1287
  ?? derived?.pageBg
1134
1288
  ?? fallback.bg,
1135
1289
  panel:
1136
1290
  exported?.cardBg
1291
+ ?? themeCardBg
1137
1292
  ?? derived?.cardBg
1138
1293
  ?? safeThemeColor(() => theme.getBgAnsi("toolPendingBg"))
1294
+ ?? readThemeColorToken(theme, "toolPendingBg")
1139
1295
  ?? fallback.panel,
1140
1296
  panel2:
1141
- derived?.panel2
1297
+ themePanel2
1298
+ ?? derived?.panel2
1142
1299
  ?? safeThemeColor(() => theme.getBgAnsi("selectedBg"))
1300
+ ?? readThemeColorToken(theme, "selectedBg")
1143
1301
  ?? exported?.infoBg
1144
1302
  ?? fallback.panel2,
1145
- border: safeThemeColor(() => theme.getFgAnsi("border")) ?? fallback.border,
1146
- borderMuted: safeThemeColor(() => theme.getFgAnsi("borderMuted")) ?? fallback.borderMuted,
1147
- text: safeThemeColor(() => theme.getFgAnsi("text")) ?? fallback.text,
1148
- muted: safeThemeColor(() => theme.getFgAnsi("muted")) ?? fallback.muted,
1303
+ border: safeThemeColor(() => theme.getFgAnsi("border")) ?? readThemeColorToken(theme, "border") ?? fallback.border,
1304
+ borderMuted: safeThemeColor(() => theme.getFgAnsi("borderMuted")) ?? readThemeColorToken(theme, "borderMuted") ?? fallback.borderMuted,
1305
+ text,
1306
+ muted: safeThemeColor(() => theme.getFgAnsi("muted")) ?? readThemeColorToken(theme, "muted") ?? fallback.muted,
1149
1307
  accent,
1150
1308
  warn,
1151
1309
  error,
@@ -1156,28 +1314,33 @@ function getStudioThemeStyle(theme?: Theme): StudioThemeStyle {
1156
1314
  accentSoftStrong: withAlpha(accent, mode === "light" ? 0.35 : 0.40, fallback.accentSoftStrong),
1157
1315
  okBorder: withAlpha(ok, mode === "light" ? 0.55 : 0.70, fallback.okBorder),
1158
1316
  warnBorder: withAlpha(warn, mode === "light" ? 0.55 : 0.70, fallback.warnBorder),
1159
- mdHeading: safeThemeColor(() => theme.getFgAnsi("mdHeading")) ?? fallback.mdHeading,
1160
- mdLink: safeThemeColor(() => theme.getFgAnsi("mdLink")) ?? fallback.mdLink,
1161
- mdLinkUrl: safeThemeColor(() => theme.getFgAnsi("mdLinkUrl")) ?? fallback.mdLinkUrl,
1162
- mdCode: safeThemeColor(() => theme.getFgAnsi("mdCode")) ?? fallback.mdCode,
1163
- mdCodeBlock: safeThemeColor(() => theme.getFgAnsi("mdCodeBlock")) ?? fallback.mdCodeBlock,
1164
- mdCodeBlockBorder: safeThemeColor(() => theme.getFgAnsi("mdCodeBlockBorder")) ?? fallback.mdCodeBlockBorder,
1165
- mdQuote: safeThemeColor(() => theme.getFgAnsi("mdQuote")) ?? fallback.mdQuote,
1166
- mdQuoteBorder: safeThemeColor(() => theme.getFgAnsi("mdQuoteBorder")) ?? fallback.mdQuoteBorder,
1167
- mdHr: safeThemeColor(() => theme.getFgAnsi("mdHr")) ?? fallback.mdHr,
1168
- mdListBullet: safeThemeColor(() => theme.getFgAnsi("mdListBullet")) ?? fallback.mdListBullet,
1169
- syntaxComment: safeThemeColor(() => theme.getFgAnsi("syntaxComment")) ?? fallback.syntaxComment,
1170
- syntaxKeyword: safeThemeColor(() => theme.getFgAnsi("syntaxKeyword")) ?? fallback.syntaxKeyword,
1171
- syntaxFunction: safeThemeColor(() => theme.getFgAnsi("syntaxFunction")) ?? fallback.syntaxFunction,
1172
- syntaxVariable: safeThemeColor(() => theme.getFgAnsi("syntaxVariable")) ?? fallback.syntaxVariable,
1173
- syntaxString: safeThemeColor(() => theme.getFgAnsi("syntaxString")) ?? fallback.syntaxString,
1174
- syntaxNumber: safeThemeColor(() => theme.getFgAnsi("syntaxNumber")) ?? fallback.syntaxNumber,
1175
- syntaxType: safeThemeColor(() => theme.getFgAnsi("syntaxType")) ?? fallback.syntaxType,
1176
- syntaxOperator: safeThemeColor(() => theme.getFgAnsi("syntaxOperator")) ?? fallback.syntaxOperator,
1177
- syntaxPunctuation: safeThemeColor(() => theme.getFgAnsi("syntaxPunctuation")) ?? fallback.syntaxPunctuation,
1317
+ mdHeading: safeThemeColor(() => theme.getFgAnsi("mdHeading")) ?? readThemeColorToken(theme, "mdHeading") ?? fallback.mdHeading,
1318
+ mdLink: safeThemeColor(() => theme.getFgAnsi("mdLink")) ?? readThemeColorToken(theme, "mdLink") ?? fallback.mdLink,
1319
+ mdLinkUrl: safeThemeColor(() => theme.getFgAnsi("mdLinkUrl")) ?? readThemeColorToken(theme, "mdLinkUrl") ?? fallback.mdLinkUrl,
1320
+ mdCode: safeThemeColor(() => theme.getFgAnsi("mdCode")) ?? readThemeColorToken(theme, "mdCode") ?? fallback.mdCode,
1321
+ mdCodeBlock: safeThemeColor(() => theme.getFgAnsi("mdCodeBlock")) ?? readThemeColorToken(theme, "mdCodeBlock") ?? text,
1322
+ mdCodeBlockBorder: safeThemeColor(() => theme.getFgAnsi("mdCodeBlockBorder")) ?? readThemeColorToken(theme, "mdCodeBlockBorder") ?? fallback.mdCodeBlockBorder,
1323
+ mdQuote: safeThemeColor(() => theme.getFgAnsi("mdQuote")) ?? readThemeColorToken(theme, "mdQuote") ?? fallback.mdQuote,
1324
+ mdQuoteBorder: safeThemeColor(() => theme.getFgAnsi("mdQuoteBorder")) ?? readThemeColorToken(theme, "mdQuoteBorder") ?? fallback.mdQuoteBorder,
1325
+ mdHr: safeThemeColor(() => theme.getFgAnsi("mdHr")) ?? readThemeColorToken(theme, "mdHr") ?? fallback.mdHr,
1326
+ mdListBullet: safeThemeColor(() => theme.getFgAnsi("mdListBullet")) ?? readThemeColorToken(theme, "mdListBullet") ?? fallback.mdListBullet,
1327
+ syntaxComment: safeThemeColor(() => theme.getFgAnsi("syntaxComment")) ?? readThemeColorToken(theme, "syntaxComment") ?? fallback.syntaxComment,
1328
+ syntaxKeyword: safeThemeColor(() => theme.getFgAnsi("syntaxKeyword")) ?? readThemeColorToken(theme, "syntaxKeyword") ?? fallback.syntaxKeyword,
1329
+ syntaxFunction: safeThemeColor(() => theme.getFgAnsi("syntaxFunction")) ?? readThemeColorToken(theme, "syntaxFunction") ?? fallback.syntaxFunction,
1330
+ syntaxVariable: safeThemeColor(() => theme.getFgAnsi("syntaxVariable")) ?? readThemeColorToken(theme, "syntaxVariable") ?? fallback.syntaxVariable,
1331
+ syntaxString: safeThemeColor(() => theme.getFgAnsi("syntaxString")) ?? readThemeColorToken(theme, "syntaxString") ?? fallback.syntaxString,
1332
+ syntaxNumber: safeThemeColor(() => theme.getFgAnsi("syntaxNumber")) ?? readThemeColorToken(theme, "syntaxNumber") ?? fallback.syntaxNumber,
1333
+ syntaxType: safeThemeColor(() => theme.getFgAnsi("syntaxType")) ?? readThemeColorToken(theme, "syntaxType") ?? fallback.syntaxType,
1334
+ syntaxOperator: safeThemeColor(() => theme.getFgAnsi("syntaxOperator")) ?? readThemeColorToken(theme, "syntaxOperator") ?? fallback.syntaxOperator,
1335
+ syntaxPunctuation: safeThemeColor(() => theme.getFgAnsi("syntaxPunctuation")) ?? readThemeColorToken(theme, "syntaxPunctuation") ?? fallback.syntaxPunctuation,
1178
1336
  };
1179
1337
 
1180
- return { mode, palette };
1338
+ return {
1339
+ mode,
1340
+ palette,
1341
+ accentContrast: readThemeVarColor(theme, ["studioAccentText", "studioAccentContrast"]),
1342
+ errorContrast: readThemeVarColor(theme, ["studioErrorText", "studioErrorContrast"]),
1343
+ };
1181
1344
  }
1182
1345
 
1183
1346
  function createSessionToken(): string {
@@ -5966,6 +6129,17 @@ function buildTerminalSessionLabel(cwd: string, sessionName?: string): string {
5966
6129
  return parts.join(" · ");
5967
6130
  }
5968
6131
 
6132
+ function buildTerminalSessionDetail(cwd: string, sessionName?: string): string {
6133
+ const termProgram = String(process.env.TERM_PROGRAM ?? "").trim() || "unknown";
6134
+ const name = String(sessionName ?? "").trim() || "unknown";
6135
+ const workingDir = String(cwd || process.cwd() || "").trim() || "unknown";
6136
+ return [
6137
+ `Terminal: ${termProgram}`,
6138
+ `Session: ${name}`,
6139
+ `Working dir: ${workingDir}`,
6140
+ ].join("\n");
6141
+ }
6142
+
5969
6143
  function sanitizePdfFilename(input: string | undefined): string {
5970
6144
  const fallback = "studio-preview.pdf";
5971
6145
  const raw = String(input ?? "").trim();
@@ -5984,12 +6158,19 @@ function sanitizePdfFilename(input: string | undefined): string {
5984
6158
  }
5985
6159
 
5986
6160
  function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
6161
+ const shadowColor = style.mode === "light"
6162
+ ? withAlpha(style.palette.text, 0.10, "rgba(15, 23, 42, 0.08)")
6163
+ : "rgba(0, 0, 0, 0.32)";
5987
6164
  const panelShadow =
5988
6165
  style.mode === "light"
5989
- ? "0 1px 2px rgba(15, 23, 42, 0.03), 0 4px 14px rgba(15, 23, 42, 0.04)"
5990
- : "0 1px 2px rgba(0, 0, 0, 0.36), 0 6px 18px rgba(0, 0, 0, 0.22)";
5991
- const accentContrast = style.mode === "light" ? "#ffffff" : "#0e1616";
5992
- const errorContrast = style.mode === "light" ? "#ffffff" : "#0e1616";
6166
+ ? `0 1px 2px ${withAlpha(style.palette.text, 0.035, "rgba(15, 23, 42, 0.03)")}, 0 4px 14px ${withAlpha(style.palette.text, 0.055, "rgba(15, 23, 42, 0.04)")}`
6167
+ : "0 1px 2px rgba(0, 0, 0, 0.30), 0 6px 18px rgba(0, 0, 0, 0.18)";
6168
+ const borderSubtle = blendColors(style.palette.borderMuted, style.palette.panel, style.mode === "light" ? 0.58 : 0.48);
6169
+ const panelBorder = blendColors(style.palette.borderMuted, style.palette.panel, style.mode === "light" ? 0.42 : 0.36);
6170
+ const controlBorder = blendColors(style.palette.borderMuted, style.palette.panel, style.mode === "light" ? 0.30 : 0.22);
6171
+ const paneActiveBorder = blendColors(style.palette.border, style.palette.panel, style.mode === "light" ? 0.34 : 0.48);
6172
+ const accentContrast = style.accentContrast ?? (style.mode === "light" ? "#ffffff" : "#0e1616");
6173
+ const errorContrast = style.errorContrast ?? readableTextOn(style.palette.error);
5993
6174
  const blockquoteBg = withAlpha(
5994
6175
  style.palette.mdQuoteBorder,
5995
6176
  style.mode === "light" ? 0.10 : 0.16,
@@ -6000,10 +6181,29 @@ function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
6000
6181
  style.mode === "light" ? 0.10 : 0.14,
6001
6182
  style.mode === "light" ? "rgba(15, 23, 42, 0.03)" : "rgba(255, 255, 255, 0.04)",
6002
6183
  );
6003
- const editorBg = style.mode === "light"
6004
- ? blendColors(style.palette.panel, "#ffffff", 0.5)
6184
+ const inlineCodeBg = withAlpha(
6185
+ style.palette.mdCodeBlockBorder,
6186
+ style.mode === "light" ? 0.13 : 0.18,
6187
+ style.mode === "light" ? "rgba(15, 23, 42, 0.06)" : "rgba(255, 255, 255, 0.07)",
6188
+ );
6189
+ const diffAddedBg = withAlpha(style.palette.ok, style.mode === "light" ? 0.10 : 0.14, "rgba(46, 160, 67, 0.12)");
6190
+ const diffRemovedBg = withAlpha(style.palette.error, style.mode === "light" ? 0.10 : 0.14, "rgba(248, 81, 73, 0.12)");
6191
+ const okSoft = withAlpha(style.palette.ok, style.mode === "light" ? 0.10 : 0.12, "rgba(115, 209, 61, 0.08)");
6192
+ const errorSoft = withAlpha(style.palette.error, style.mode === "light" ? 0.10 : 0.12, "rgba(255, 107, 107, 0.08)");
6193
+ const backdropBg = style.mode === "light" ? "rgba(15, 23, 42, 0.20)" : "rgba(0, 0, 0, 0.48)";
6194
+ const panelLum = hexToRgb(style.palette.panel) ? relativeLuminance(style.palette.panel) : null;
6195
+ const panel2Lum = hexToRgb(style.palette.panel2) ? relativeLuminance(style.palette.panel2) : null;
6196
+ const lightPrimarySurface = panelLum != null && panel2Lum != null && panel2Lum > panelLum
6197
+ ? style.palette.panel2
6005
6198
  : style.palette.panel;
6199
+ const lightSecondarySurface = lightPrimarySurface === style.palette.panel ? style.palette.panel2 : style.palette.panel;
6200
+ const editorBg = style.mode === "light" ? lightPrimarySurface : style.palette.panel;
6201
+ const editorGutterBg = style.mode === "light" ? lightSecondarySurface : style.palette.panel2;
6202
+ const referenceMetaBg = style.mode === "light" ? lightSecondarySurface : style.palette.panel2;
6203
+ const referenceBadgeBg = style.mode === "light" ? lightPrimarySurface : style.palette.panel;
6006
6204
  const monoFontStack = getStudioMonoFontStack();
6205
+ const uiFontStack = getStudioUiFontStack();
6206
+ const proseFontStack = getStudioProseFontStack();
6007
6207
 
6008
6208
  return {
6009
6209
  "color-scheme": style.mode,
@@ -6012,6 +6212,10 @@ function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
6012
6212
  "--panel-2": style.palette.panel2,
6013
6213
  "--border": style.palette.border,
6014
6214
  "--border-muted": style.palette.borderMuted,
6215
+ "--border-subtle": borderSubtle,
6216
+ "--panel-border": panelBorder,
6217
+ "--control-border": controlBorder,
6218
+ "--pane-active-border": paneActiveBorder,
6015
6219
  "--text": style.palette.text,
6016
6220
  "--muted": style.palette.muted,
6017
6221
  "--accent": style.palette.accent,
@@ -6044,11 +6248,24 @@ function buildThemeCssVars(style: StudioThemeStyle): Record<string, string> {
6044
6248
  "--syntax-operator": style.palette.syntaxOperator,
6045
6249
  "--syntax-punctuation": style.palette.syntaxPunctuation,
6046
6250
  "--panel-shadow": panelShadow,
6251
+ "--shadow-color": shadowColor,
6047
6252
  "--accent-contrast": accentContrast,
6048
6253
  "--error-contrast": errorContrast,
6049
6254
  "--blockquote-bg": blockquoteBg,
6255
+ "--inline-code-bg": inlineCodeBg,
6050
6256
  "--table-alt-bg": tableAltBg,
6257
+ "--md-table-border": borderSubtle,
6258
+ "--diff-added-bg": diffAddedBg,
6259
+ "--diff-removed-bg": diffRemovedBg,
6260
+ "--ok-soft": okSoft,
6261
+ "--error-soft": errorSoft,
6262
+ "--backdrop-bg": backdropBg,
6051
6263
  "--editor-bg": editorBg,
6264
+ "--editor-gutter-bg": editorGutterBg,
6265
+ "--reference-meta-bg": referenceMetaBg,
6266
+ "--reference-badge-bg": referenceBadgeBg,
6267
+ "--font-ui": uiFontStack,
6268
+ "--font-prose": proseFontStack,
6052
6269
  "--font-mono": monoFontStack,
6053
6270
  };
6054
6271
  }
@@ -6069,6 +6286,7 @@ function buildStudioHtml(
6069
6286
  theme?: Theme,
6070
6287
  initialModelLabel?: string,
6071
6288
  initialTerminalLabel?: string,
6289
+ initialTerminalDetail?: string,
6072
6290
  initialContextUsage?: StudioContextUsageSnapshot,
6073
6291
  studioMode: StudioUiMode = "full",
6074
6292
  ): string {
@@ -6079,6 +6297,7 @@ function buildStudioHtml(
6079
6297
  const initialDraftId = escapeHtmlForInline(initialDocument?.draftId ?? "");
6080
6298
  const initialModel = escapeHtmlForInline(initialModelLabel ?? "none");
6081
6299
  const initialTerminal = escapeHtmlForInline(initialTerminalLabel ?? "unknown");
6300
+ const initialTerminalDetailAttr = escapeHtmlForInline(initialTerminalDetail ?? initialTerminalLabel ?? "unknown");
6082
6301
  const initialContextTokens =
6083
6302
  typeof initialContextUsage?.tokens === "number" && Number.isFinite(initialContextUsage.tokens)
6084
6303
  ? String(initialContextUsage.tokens)
@@ -6145,7 +6364,7 @@ ${cssVarsBlock}
6145
6364
  </style>
6146
6365
  <link rel="stylesheet" href="${stylesheetHref}" />
6147
6366
  </head>
6148
- <body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}">
6367
+ <body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-terminal-detail="${initialTerminalDetailAttr}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}">
6149
6368
  <header>
6150
6369
  <h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
6151
6370
  <div class="controls">
@@ -6241,6 +6460,16 @@ ${cssVarsBlock}
6241
6460
  <option value="off">Line numbers: Off</option>
6242
6461
  <option value="on" selected>Line numbers: On</option>
6243
6462
  </select>
6463
+ <select id="editorFontSizeSelect" aria-label="Editor text size" title="Adjust raw editor text size.">
6464
+ <option value="10">Editor text: 10px</option>
6465
+ <option value="11">Editor text: 11px</option>
6466
+ <option value="12" selected>Editor text: 12px</option>
6467
+ <option value="13">Editor text: 13px</option>
6468
+ <option value="14">Editor text: 14px</option>
6469
+ <option value="15">Editor text: 15px</option>
6470
+ <option value="16">Editor text: 16px</option>
6471
+ <option value="18">Editor text: 18px</option>
6472
+ </select>
6244
6473
  </div>
6245
6474
  </div>
6246
6475
  </div>
@@ -6334,7 +6563,7 @@ ${cssVarsBlock}
6334
6563
  <div id="critiqueView" class="panel-scroll rendered-markdown"><pre class="plain-markdown">No response yet.</pre></div>
6335
6564
  <div class="response-wrap">
6336
6565
  <div id="responseActions" class="response-actions">
6337
- <div class="response-actions-row">
6566
+ <div class="response-actions-row response-options-row">
6338
6567
  <select id="followSelect" aria-label="Auto-update response">
6339
6568
  <option value="on" selected>Auto-update response: On</option>
6340
6569
  <option value="off">Auto-update response: Off</option>
@@ -6343,6 +6572,20 @@ ${cssVarsBlock}
6343
6572
  <option value="off">Syntax highlight: Off</option>
6344
6573
  <option value="on" selected>Syntax highlight: On</option>
6345
6574
  </select>
6575
+ <select id="responseFontSizeSelect" aria-label="Response text size" title="Adjust right-pane response, preview, and working text size.">
6576
+ <option value="11">Response text: 11px</option>
6577
+ <option value="12">Response text: 12px</option>
6578
+ <option value="12.5">Response text: 12.5px</option>
6579
+ <option value="13">Response text: 13px</option>
6580
+ <option value="13.5" selected>Response text: 13.5px</option>
6581
+ <option value="14">Response text: 14px</option>
6582
+ <option value="14.5">Response text: 14.5px</option>
6583
+ <option value="15">Response text: 15px</option>
6584
+ <option value="15.5">Response text: 15.5px</option>
6585
+ <option value="16">Response text: 16px</option>
6586
+ <option value="18">Response text: 18px</option>
6587
+ <option value="20">Response text: 20px</option>
6588
+ </select>
6346
6589
  </div>
6347
6590
  <div class="response-actions-row history-row">
6348
6591
  <button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Fetch latest response</button>
@@ -6351,7 +6594,7 @@ ${cssVarsBlock}
6351
6594
  <button id="historyNextBtn" type="button" title="Show next response in history.">Next response ▶</button>
6352
6595
  <button id="historyLastBtn" type="button" title="Jump to the latest loaded response in history.">Last response ▶|</button>
6353
6596
  </div>
6354
- <div class="response-actions-row">
6597
+ <div class="response-actions-row response-result-row">
6355
6598
  <button id="loadResponseBtn" type="button">Load response into editor</button>
6356
6599
  <button id="loadCritiqueNotesBtn" type="button" hidden>Load critique notes into editor</button>
6357
6600
  <button id="loadCritiqueFullBtn" type="button" hidden>Load full critique into editor</button>
@@ -6365,7 +6608,7 @@ ${cssVarsBlock}
6365
6608
 
6366
6609
  <footer>
6367
6610
  <span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
6368
- <span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text">Model: ${initialModel} · Terminal: ${initialTerminal} · Context: unknown</span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
6611
+ <span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text"><span id="footerMetaModel" class="footer-meta-part footer-meta-model">${initialModel}</span><span class="footer-meta-sep">·</span><span id="footerMetaTerminal" class="footer-meta-part footer-meta-terminal">${initialTerminal}</span><span class="footer-meta-sep">·</span><span id="footerMetaContext" class="footer-meta-part footer-meta-context">unknown</span></span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
6369
6612
  <span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Save editor: Cmd/Ctrl+S · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
6370
6613
  </footer>
6371
6614
 
@@ -6424,6 +6667,7 @@ export default function (pi: ExtensionAPI) {
6424
6667
  let currentModel: { provider?: string; id?: string } | undefined;
6425
6668
  let currentModelLabel = "none";
6426
6669
  let terminalSessionLabel = buildTerminalSessionLabel(studioCwd);
6670
+ let terminalSessionDetail = buildTerminalSessionDetail(studioCwd);
6427
6671
  let studioResponseHistory: StudioResponseHistoryItem[] = [];
6428
6672
  let latestSessionUserPrompt: string | null = null;
6429
6673
  let pendingTurnPrompt: string | null = null;
@@ -6508,6 +6752,7 @@ export default function (pi: ExtensionAPI) {
6508
6752
  const baseModelLabel = formatModelLabel(currentModel);
6509
6753
  currentModelLabel = formatModelLabelWithThinking(baseModelLabel, getThinkingLevelSafe());
6510
6754
  terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
6755
+ terminalSessionDetail = buildTerminalSessionDetail(studioCwd, getSessionNameSafe());
6511
6756
  };
6512
6757
 
6513
6758
  const notifyStudio = (message: string, level: "info" | "warning" | "error" = "info") => {
@@ -7081,6 +7326,7 @@ export default function (pi: ExtensionAPI) {
7081
7326
 
7082
7327
  const broadcastState = () => {
7083
7328
  terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
7329
+ terminalSessionDetail = buildTerminalSessionDetail(studioCwd, getSessionNameSafe());
7084
7330
  currentModelLabel = formatModelLabelWithThinking(formatModelLabel(currentModel), getThinkingLevelSafe());
7085
7331
  refreshContextUsage();
7086
7332
  broadcast({
@@ -7092,6 +7338,7 @@ export default function (pi: ExtensionAPI) {
7092
7338
  terminalActivityLabel,
7093
7339
  modelLabel: currentModelLabel,
7094
7340
  terminalSessionLabel,
7341
+ terminalSessionDetail,
7095
7342
  contextTokens: contextUsageSnapshot.tokens,
7096
7343
  contextWindow: contextUsageSnapshot.contextWindow,
7097
7344
  contextPercent: contextUsageSnapshot.percent,
@@ -7386,6 +7633,7 @@ export default function (pi: ExtensionAPI) {
7386
7633
  terminalActivityLabel,
7387
7634
  modelLabel: currentModelLabel,
7388
7635
  terminalSessionLabel,
7636
+ terminalSessionDetail,
7389
7637
  contextTokens: contextUsageSnapshot.tokens,
7390
7638
  contextWindow: contextUsageSnapshot.contextWindow,
7391
7639
  contextPercent: contextUsageSnapshot.percent,
@@ -8511,7 +8759,7 @@ export default function (pi: ExtensionAPI) {
8511
8759
  refreshContextUsage();
8512
8760
  const studioMode = normalizeStudioUiMode(requestUrl.searchParams.get("mode"));
8513
8761
  const requestInitialDocument = resolveRequestedStudioDocumentFromUrl(requestUrl, initialStudioDocument, studioCwd, lastStudioResponse);
8514
- res.end(buildStudioHtml(requestInitialDocument, serverState.token, lastCommandCtx?.ui.theme, currentModelLabel, terminalSessionLabel, contextUsageSnapshot, studioMode));
8762
+ res.end(buildStudioHtml(requestInitialDocument, serverState.token, lastCommandCtx?.ui.theme, currentModelLabel, terminalSessionLabel, terminalSessionDetail, contextUsageSnapshot, studioMode));
8515
8763
  };
8516
8764
 
8517
8765
  const ensureServer = async (): Promise<StudioServerState> => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -13,12 +13,15 @@
13
13
  "url": "https://github.com/omaclaren/pi-studio/issues"
14
14
  },
15
15
  "keywords": [
16
- "pi-package"
16
+ "pi-package",
17
+ "pi-extension",
18
+ "pi-theme"
17
19
  ],
18
20
  "files": [
19
21
  "index.ts",
20
22
  "client",
21
23
  "shared",
24
+ "themes",
22
25
  "README.md",
23
26
  "CHANGELOG.md",
24
27
  "WORKFLOW.md",
@@ -31,6 +34,9 @@
31
34
  "pi": {
32
35
  "extensions": [
33
36
  "./index.ts"
37
+ ],
38
+ "themes": [
39
+ "./themes"
34
40
  ]
35
41
  },
36
42
  "peerDependencies": {