pi-one-ui 0.1.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.
Files changed (101) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -0
  3. package/extensions/app/config/renderer.ts +452 -0
  4. package/extensions/app/config/shell.ts +2484 -0
  5. package/extensions/app/ownership.ts +18 -0
  6. package/extensions/app/panel.ts +533 -0
  7. package/extensions/app/presets.ts +36 -0
  8. package/extensions/features/agent-summary/core.ts +194 -0
  9. package/extensions/features/agent-summary/index.ts +37 -0
  10. package/extensions/features/compact-thinking.ts +1262 -0
  11. package/extensions/features/context.ts +857 -0
  12. package/extensions/features/reference/index.ts +643 -0
  13. package/extensions/features/reference/session.ts +278 -0
  14. package/extensions/features/reference/subagent.ts +218 -0
  15. package/extensions/features/shell/aliases.ts +28 -0
  16. package/extensions/features/shell/flush-docked-bash.ts +39 -0
  17. package/extensions/features/shell/startup-header.ts +357 -0
  18. package/extensions/features/shell/working-message.ts +226 -0
  19. package/extensions/index.ts +52 -0
  20. package/extensions/shell/accent-rail-editor.ts +112 -0
  21. package/extensions/shell/accent-rail-layout-patch.ts +530 -0
  22. package/extensions/shell/completion-menu.ts +186 -0
  23. package/extensions/shell/editor-metadata-format.ts +287 -0
  24. package/extensions/shell/editor-transfer.ts +73 -0
  25. package/extensions/shell/extension-status.ts +102 -0
  26. package/extensions/shell/footer-format.ts +388 -0
  27. package/extensions/shell/footer-layout.ts +144 -0
  28. package/extensions/shell/footer.ts +1035 -0
  29. package/extensions/shell/format.ts +577 -0
  30. package/extensions/shell/git.ts +434 -0
  31. package/extensions/shell/icons.ts +261 -0
  32. package/extensions/shell/index.ts +1659 -0
  33. package/extensions/shell/interaction-summary.ts +1194 -0
  34. package/extensions/shell/live-context.ts +82 -0
  35. package/extensions/shell/minimalist-editor.ts +548 -0
  36. package/extensions/shell/package-version.ts +675 -0
  37. package/extensions/shell/project-refresh.ts +130 -0
  38. package/extensions/shell/project-state.ts +60 -0
  39. package/extensions/shell/prototype-patch-registry.ts +183 -0
  40. package/extensions/shell/runtime.ts +915 -0
  41. package/extensions/shell/selector-border.ts +109 -0
  42. package/extensions/shell/session-lifecycle.ts +60 -0
  43. package/extensions/shell/settings-command.ts +2144 -0
  44. package/extensions/shell/settings-previews.ts +137 -0
  45. package/extensions/shell/state.ts +83 -0
  46. package/extensions/shell/style.ts +480 -0
  47. package/extensions/shell/telemetry.ts +90 -0
  48. package/extensions/shell/ui.ts +1326 -0
  49. package/extensions/shell/user-message-osc.ts +279 -0
  50. package/extensions/shell/user-message-styles.ts +266 -0
  51. package/extensions/shell/user-message.ts +226 -0
  52. package/extensions/shell/working-line-messages.ts +24 -0
  53. package/extensions/shell/working-line-spinners.ts +54 -0
  54. package/extensions/shell/working-line.ts +1603 -0
  55. package/extensions/tools/ansi-color.ts +44 -0
  56. package/extensions/tools/ansi-text.ts +52 -0
  57. package/extensions/tools/component-tree.ts +37 -0
  58. package/extensions/tools/format.ts +28 -0
  59. package/extensions/tools/fullscreen-detect.ts +11 -0
  60. package/extensions/tools/patch-keys.ts +158 -0
  61. package/extensions/tools/sgr-mouse.ts +59 -0
  62. package/extensions/tools/tool-loading-icon.ts +20 -0
  63. package/extensions/tools/tool-result-sanitize.ts +27 -0
  64. package/extensions/transcript/index.ts +65 -0
  65. package/extensions/transcript/renderer/compact-mode.ts +1443 -0
  66. package/extensions/transcript/renderer/default-mode.ts +735 -0
  67. package/extensions/transcript/renderer/index.ts +286 -0
  68. package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
  69. package/extensions/transcript/renderer/mouse/hover.ts +175 -0
  70. package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
  71. package/extensions/transcript/renderer/mouse/layout.ts +207 -0
  72. package/extensions/transcript/renderer/mouse/packets.ts +96 -0
  73. package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
  74. package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
  75. package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
  76. package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
  77. package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
  78. package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
  79. package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
  80. package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
  81. package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
  82. package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
  83. package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
  84. package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
  85. package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
  86. package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
  87. package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
  88. package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
  89. package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
  90. package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
  91. package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
  92. package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
  93. package/extensions/transcript/renderer/tool/grouping.ts +619 -0
  94. package/extensions/transcript/renderer/tool/message-display.ts +183 -0
  95. package/extensions/transcript/renderer/tool/names.ts +195 -0
  96. package/extensions/transcript/renderer/tool/result.ts +842 -0
  97. package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
  98. package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
  99. package/package.json +58 -0
  100. package/themes/cc-dark.json +91 -0
  101. package/themes/cc-light.json +91 -0
@@ -0,0 +1,189 @@
1
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
2
+ import {
3
+ DEFAULT_TOOL_DISPLAY_CONFIG,
4
+ type ToolDisplayConfig,
5
+ } from "../../../../app/config/renderer.ts";
6
+ import { showMoreHintText } from "../show-more-hint.ts";
7
+ import {
8
+ buildCollapsedDiffHintText,
9
+ clampRenderedLineToWidth,
10
+ clampRenderedLinesToWidth,
11
+ } from "./line-width-safety.ts";
12
+ import { stabilizeBackgroundResets, type DiffTheme } from "./diff-palette.ts";
13
+ import { countDiffLineEntries, type ParsedDiffEntry } from "./diff-parse.ts";
14
+ import type { SplitDiffRow } from "./diff-inline.ts";
15
+ import type { RenderedRow } from "./diff-layout.ts";
16
+
17
+ const DIFF_WIDTH_OPS = {
18
+ measure: visibleWidth,
19
+ truncate: (text: string, maxWidth: number): string =>
20
+ truncateToWidth(text, maxWidth, ""),
21
+ };
22
+
23
+ export function clampDiffLineToWidth(text: string, width: number): string {
24
+ return stabilizeBackgroundResets(
25
+ clampRenderedLineToWidth(text, width, DIFF_WIDTH_OPS),
26
+ );
27
+ }
28
+
29
+ export function clampDiffLinesToWidth(
30
+ lines: string[],
31
+ width: number,
32
+ ): string[] {
33
+ return clampRenderedLinesToWidth(lines, width, DIFF_WIDTH_OPS).map((line) =>
34
+ stabilizeBackgroundResets(line),
35
+ );
36
+ }
37
+
38
+ function renderDiffSpacerLine(width: number): string {
39
+ const safeWidth = Math.max(0, width);
40
+ return safeWidth > 0 ? " ".repeat(safeWidth) : "";
41
+ }
42
+
43
+ export function resolveDiffDisplayLimit(
44
+ expanded: boolean,
45
+ maxCollapsedLines: number,
46
+ maxExpandedLines: number,
47
+ ): number {
48
+ const expandedLimit = Number.isFinite(maxExpandedLines)
49
+ ? maxExpandedLines
50
+ : DEFAULT_TOOL_DISPLAY_CONFIG.expandedPreviewMaxLines;
51
+ const collapsedLimit = Number.isFinite(maxCollapsedLines)
52
+ ? maxCollapsedLines
53
+ : DEFAULT_TOOL_DISPLAY_CONFIG.editDiffCollapsedLines;
54
+ return expanded ? Math.max(0, expandedLimit) : Math.max(1, collapsedLimit);
55
+ }
56
+
57
+ /** Write collapsed body limit. Missing/NaN falls back to edit's `editDiffCollapsedLines`. */
58
+ export function resolveWriteCollapsedLimit(
59
+ config: Pick<
60
+ ToolDisplayConfig,
61
+ "writeDiffCollapsedLines" | "editDiffCollapsedLines"
62
+ >,
63
+ ): number {
64
+ const value = config.writeDiffCollapsedLines;
65
+ if (Number.isFinite(value)) {
66
+ return Math.max(0, value);
67
+ }
68
+ const fallback = Number.isFinite(config.editDiffCollapsedLines)
69
+ ? config.editDiffCollapsedLines
70
+ : DEFAULT_TOOL_DISPLAY_CONFIG.writeDiffCollapsedLines;
71
+ return Math.max(0, fallback);
72
+ }
73
+
74
+ /**
75
+ * How many source rows/entries to fully highlight+render before applyLineLimit.
76
+ * Extra headroom covers word-wrap expansion so the display limit can still fill.
77
+ */
78
+ export function resolveDiffProcessBudget(
79
+ displayLimit: number,
80
+ wordWrap: boolean,
81
+ ): number {
82
+ if (!Number.isFinite(displayLimit) || displayLimit <= 0) {
83
+ return Number.POSITIVE_INFINITY;
84
+ }
85
+ return wordWrap ? displayLimit * 2 + 8 : displayLimit + 4;
86
+ }
87
+
88
+ export function takeEntriesForLineBudget(
89
+ entries: ParsedDiffEntry[],
90
+ maxLineEntries: number,
91
+ ): {
92
+ entries: ParsedDiffEntry[];
93
+ totalLineEntries: number;
94
+ processedLineEntries: number;
95
+ } {
96
+ const totalLineEntries = countDiffLineEntries(entries);
97
+ if (!Number.isFinite(maxLineEntries) || maxLineEntries >= totalLineEntries) {
98
+ return {
99
+ entries,
100
+ totalLineEntries,
101
+ processedLineEntries: totalLineEntries,
102
+ };
103
+ }
104
+ const limited: ParsedDiffEntry[] = [];
105
+ let processedLineEntries = 0;
106
+ for (const entry of entries) {
107
+ if (entry.kind === "line") {
108
+ if (processedLineEntries >= maxLineEntries) break;
109
+ processedLineEntries++;
110
+ }
111
+ limited.push(entry);
112
+ }
113
+ return { entries: limited, totalLineEntries, processedLineEntries };
114
+ }
115
+
116
+ export function takeSplitRowsForBudget(
117
+ rows: SplitDiffRow[],
118
+ maxRows: number,
119
+ ): { rows: SplitDiffRow[]; totalRows: number; processedRows: number } {
120
+ const totalRows = rows.length;
121
+ if (!Number.isFinite(maxRows) || maxRows >= totalRows) {
122
+ return { rows, totalRows, processedRows: totalRows };
123
+ }
124
+ const limited = rows.slice(0, maxRows);
125
+ return { rows: limited, totalRows, processedRows: limited.length };
126
+ }
127
+
128
+ export function applyLineLimit(
129
+ rows: RenderedRow[],
130
+ width: number,
131
+ expanded: boolean,
132
+ maxCollapsedLines: number,
133
+ maxExpandedLines: number,
134
+ totalHunks: number,
135
+ theme: DiffTheme,
136
+ /**
137
+ * When the caller only rendered a prefix of the full diff, pass the unprocessed
138
+ * remainder so the collapse hint still reflects total hidden content.
139
+ */
140
+ unprocessedLogicalRows = 0,
141
+ hovered = false,
142
+ ): string[] {
143
+ const limit = resolveDiffDisplayLimit(
144
+ expanded,
145
+ maxCollapsedLines,
146
+ maxExpandedLines,
147
+ );
148
+ const safeUnprocessed = Math.max(0, unprocessedLogicalRows);
149
+ if (limit === 0 || (rows.length <= limit && safeUnprocessed === 0)) {
150
+ return rows.map((row) => clampDiffLineToWidth(row.text, width));
151
+ }
152
+
153
+ const shown = rows.length <= limit ? rows : rows.slice(0, limit);
154
+ const remaining = Math.max(0, rows.length - shown.length) + safeUnprocessed;
155
+ if (remaining === 0) {
156
+ return shown.map((row) => clampDiffLineToWidth(row.text, width));
157
+ }
158
+ const visibleHunks = new Set(
159
+ shown
160
+ .map((row) => row.hunkIndex)
161
+ .filter(
162
+ (hunkIndex): hunkIndex is number =>
163
+ typeof hunkIndex === "number" && hunkIndex > 0,
164
+ ),
165
+ );
166
+ const hiddenHunks = Math.max(0, totalHunks - visibleHunks.size);
167
+ const hintText = buildCollapsedDiffHintText(
168
+ {
169
+ remainingLines: remaining,
170
+ hiddenHunks,
171
+ },
172
+ width,
173
+ DIFF_WIDTH_OPS,
174
+ );
175
+
176
+ const clickLabel = showMoreHintText();
177
+ const clickIndex = hintText.lastIndexOf(clickLabel);
178
+ const styledHint =
179
+ hovered && !expanded && clickIndex >= 0
180
+ ? theme.fg("muted", hintText.slice(0, clickIndex)) +
181
+ theme.fg("text", clickLabel) +
182
+ theme.fg("muted", hintText.slice(clickIndex + clickLabel.length))
183
+ : theme.fg(expanded ? "warning" : "muted", hintText);
184
+ return [
185
+ ...shown.map((row) => clampDiffLineToWidth(row.text, width)),
186
+ renderDiffSpacerLine(width),
187
+ clampDiffLineToWidth(styledHint, width),
188
+ ];
189
+ }
@@ -0,0 +1,436 @@
1
+ import {
2
+ ANSI_SGR_PATTERN,
3
+ expandSgrReset,
4
+ filterSgrSequences,
5
+ readSgrColorSequence,
6
+ toSgrParams,
7
+ } from "./ansi-utils.ts";
8
+ import { ansi256ToRgb } from "../../../../tools/ansi-color.ts";
9
+ import { fitToWidth } from "./diff-text.ts";
10
+ import { mergeSpans, type DiffSpan } from "./diff-inline.ts";
11
+ import type { DiffLineKind } from "./diff-parse.ts";
12
+
13
+ export interface DiffTheme {
14
+ fg(color: string, text: string): string;
15
+ bg?(color: string, text: string): string;
16
+ bold?(text: string): string;
17
+ getFgAnsi?(color: string): string;
18
+ getBgAnsi?(color: string): string;
19
+ }
20
+
21
+ export interface RgbColor {
22
+ r: number;
23
+ g: number;
24
+ b: number;
25
+ }
26
+
27
+ export interface DiffPalette {
28
+ addRowBgAnsi: string;
29
+ removeRowBgAnsi: string;
30
+ addEmphasisBgAnsi: string;
31
+ removeEmphasisBgAnsi: string;
32
+ }
33
+
34
+ const ADD_ROW_BACKGROUND_MIX_RATIO = 0.12;
35
+ const REMOVE_ROW_BACKGROUND_MIX_RATIO = 0.12;
36
+ const ADD_INLINE_EMPHASIS_MIX_RATIO = 0.26;
37
+ const REMOVE_INLINE_EMPHASIS_MIX_RATIO = 0.26;
38
+ const ADDITION_TINT_TARGET: RgbColor = { r: 84, g: 190, b: 118 };
39
+ const DELETION_TINT_TARGET: RgbColor = { r: 232, g: 95, b: 122 };
40
+ export const ANSI_BG_RESET = "\x1b[49m";
41
+
42
+ export function emphasis(theme: DiffTheme, text: string): string {
43
+ return typeof theme.bold === "function" ? theme.bold(text) : text;
44
+ }
45
+
46
+ function sequenceResetsBackground(params: number[]): boolean {
47
+ let index = 0;
48
+ while (index < params.length) {
49
+ const param = params[index] ?? 0;
50
+ if (param === 0 || param === 49) {
51
+ return true;
52
+ }
53
+
54
+ const colorSequence = readSgrColorSequence(params, index);
55
+ index += colorSequence ? colorSequence.length : 1;
56
+ }
57
+
58
+ return false;
59
+ }
60
+
61
+ function stripBackgroundResetParams(params: number[]): number[] {
62
+ const filtered: number[] = [];
63
+
64
+ for (let i = 0; i < params.length; i++) {
65
+ const param = params[i] ?? 0;
66
+
67
+ if (param === 0) {
68
+ filtered.push(...expandSgrReset(param)!);
69
+ continue;
70
+ }
71
+
72
+ if (param === 49) {
73
+ continue;
74
+ }
75
+
76
+ const colorSequence = readSgrColorSequence(params, i);
77
+ if (colorSequence) {
78
+ filtered.push(...colorSequence);
79
+ i += colorSequence.length - 1;
80
+ continue;
81
+ }
82
+
83
+ filtered.push(param);
84
+ }
85
+
86
+ return filtered;
87
+ }
88
+
89
+ export function stabilizeBackgroundResets(text: string): string {
90
+ if (!text) {
91
+ return text;
92
+ }
93
+ return filterSgrSequences(text, stripBackgroundResetParams);
94
+ }
95
+
96
+ export function keepBackgroundAcrossResets(
97
+ text: string,
98
+ rowBg: string,
99
+ ): string {
100
+ if (!text) {
101
+ return text;
102
+ }
103
+
104
+ return text.replace(ANSI_SGR_PATTERN, (sequence, rawParams: string) => {
105
+ const params = toSgrParams(rawParams);
106
+ if (params.length === 0 || !sequenceResetsBackground(params)) {
107
+ return sequence;
108
+ }
109
+ return `${sequence}${rowBg}`;
110
+ });
111
+ }
112
+
113
+ export function colorizeSegment(
114
+ theme: DiffTheme,
115
+ color: "dim" | "toolDiffAdded" | "toolDiffRemoved",
116
+ text: string,
117
+ rowBg: string | undefined,
118
+ ): string {
119
+ let themedText: string;
120
+ try {
121
+ themedText = theme.fg(color, text);
122
+ } catch {
123
+ themedText = text;
124
+ }
125
+
126
+ if (!rowBg) {
127
+ return themedText;
128
+ }
129
+
130
+ const stableText = keepBackgroundAcrossResets(themedText, rowBg);
131
+ return `${rowBg}${stableText}${rowBg}`;
132
+ }
133
+
134
+ function applyBackgroundToVisualRow(
135
+ text: string,
136
+ width: number,
137
+ rowBgAnsi: string,
138
+ restoreBgAnsi: string,
139
+ ): string {
140
+ if (width <= 0) {
141
+ return "";
142
+ }
143
+
144
+ const fitted = fitToWidth(text, width);
145
+ const withStableBackground = keepBackgroundAcrossResets(fitted, rowBgAnsi);
146
+ return stabilizeBackgroundResets(
147
+ `${rowBgAnsi}${withStableBackground}${restoreBgAnsi}`,
148
+ );
149
+ }
150
+
151
+ export function applyLineBackgroundToWrappedRows(
152
+ rows: string[],
153
+ width: number,
154
+ rowBgAnsi: string,
155
+ restoreBgAnsi: string,
156
+ ): string[] {
157
+ if (rows.length === 0) {
158
+ return [applyBackgroundToVisualRow("", width, rowBgAnsi, restoreBgAnsi)];
159
+ }
160
+
161
+ return rows.map((row) =>
162
+ applyBackgroundToVisualRow(row, width, rowBgAnsi, restoreBgAnsi),
163
+ );
164
+ }
165
+
166
+ /** utils 版返回 tuple,此处适配为本文件使用的 RgbColor 形状。 */
167
+ function ansi256ToRgbColor(code: number): RgbColor {
168
+ const [r, g, b] = ansi256ToRgb(code);
169
+ return { r, g, b };
170
+ }
171
+
172
+ export function parseAnsiColorCode(ansi: string | undefined): RgbColor | null {
173
+ if (!ansi) {
174
+ return null;
175
+ }
176
+ const rgbMatch = /\x1b\[(?:3|4)8;2;(\d{1,3});(\d{1,3});(\d{1,3})m/.exec(ansi);
177
+ if (rgbMatch) {
178
+ const r = Number.parseInt(rgbMatch[1] ?? "0", 10);
179
+ const g = Number.parseInt(rgbMatch[2] ?? "0", 10);
180
+ const b = Number.parseInt(rgbMatch[3] ?? "0", 10);
181
+ if (Number.isFinite(r) && Number.isFinite(g) && Number.isFinite(b)) {
182
+ return {
183
+ r: Math.max(0, Math.min(255, r)),
184
+ g: Math.max(0, Math.min(255, g)),
185
+ b: Math.max(0, Math.min(255, b)),
186
+ };
187
+ }
188
+ }
189
+
190
+ const bitMatch = /\x1b\[(?:3|4)8;5;(\d{1,3})m/.exec(ansi);
191
+ if (bitMatch) {
192
+ const code = Number.parseInt(bitMatch[1] ?? "0", 10);
193
+ if (Number.isFinite(code)) {
194
+ return ansi256ToRgbColor(code);
195
+ }
196
+ }
197
+
198
+ const basicMatch = /\x1b\[(?:3|4)([0-7])m/.exec(ansi);
199
+ if (basicMatch) return ansi256ToRgbColor(Number(basicMatch[1]));
200
+ const brightMatch = /\x1b\[(?:9|10)([0-7])m/.exec(ansi);
201
+ if (brightMatch) return ansi256ToRgbColor(Number(brightMatch[1]) + 8);
202
+ return null;
203
+ }
204
+
205
+ function rgbToBgAnsi(color: RgbColor): string {
206
+ const r = Math.max(0, Math.min(255, Math.round(color.r)));
207
+ const g = Math.max(0, Math.min(255, Math.round(color.g)));
208
+ const b = Math.max(0, Math.min(255, Math.round(color.b)));
209
+ return `\x1b[48;2;${r};${g};${b}m`;
210
+ }
211
+
212
+ function mixRgb(base: RgbColor, tint: RgbColor, ratio: number): RgbColor {
213
+ const clamped = Math.max(0, Math.min(1, ratio));
214
+ return {
215
+ r: base.r * (1 - clamped) + tint.r * clamped,
216
+ g: base.g * (1 - clamped) + tint.g * clamped,
217
+ b: base.b * (1 - clamped) + tint.b * clamped,
218
+ };
219
+ }
220
+
221
+ function extractThemeBackgroundAnsi(text: string): string | undefined {
222
+ if (!text || !text.includes("\x1b[")) {
223
+ return undefined;
224
+ }
225
+
226
+ ANSI_SGR_PATTERN.lastIndex = 0;
227
+ let match: RegExpExecArray | null;
228
+ while ((match = ANSI_SGR_PATTERN.exec(text)) !== null) {
229
+ const parsed = toSgrParams(match[1] ?? "");
230
+ for (let index = 0; index < parsed.length; index += 1) {
231
+ const param = parsed[index] ?? 0;
232
+ if ((param >= 40 && param <= 47) || (param >= 100 && param <= 107)) {
233
+ return `\x1b[${param}m`;
234
+ }
235
+
236
+ const colorSequence = readSgrColorSequence(parsed, index);
237
+ if (colorSequence?.[0] === 48) {
238
+ return `\x1b[${colorSequence.join(";")}m`;
239
+ }
240
+ if (colorSequence) {
241
+ index += colorSequence.length - 1;
242
+ }
243
+ }
244
+ }
245
+
246
+ return undefined;
247
+ }
248
+
249
+ export function readThemeAnsi(
250
+ theme: DiffTheme,
251
+ kind: "fg" | "bg",
252
+ slot: string,
253
+ ): string | undefined {
254
+ try {
255
+ if (kind === "fg" && typeof theme.getFgAnsi === "function") {
256
+ return theme.getFgAnsi(slot);
257
+ }
258
+ if (kind === "bg") {
259
+ if (typeof theme.getBgAnsi === "function") {
260
+ return theme.getBgAnsi(slot);
261
+ }
262
+ if (typeof theme.bg === "function") {
263
+ return extractThemeBackgroundAnsi(theme.bg(slot, " "));
264
+ }
265
+ }
266
+ } catch {
267
+ return undefined;
268
+ }
269
+ return undefined;
270
+ }
271
+
272
+ export function resolveDiffPalette(theme: DiffTheme): DiffPalette {
273
+ const baseBg = parseAnsiColorCode(
274
+ readThemeAnsi(theme, "bg", "toolSuccessBg"),
275
+ ) ??
276
+ parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolPendingBg")) ??
277
+ parseAnsiColorCode(readThemeAnsi(theme, "bg", "userMessageBg")) ?? {
278
+ r: 32,
279
+ g: 35,
280
+ b: 42,
281
+ };
282
+ const addFg = parseAnsiColorCode(
283
+ readThemeAnsi(theme, "fg", "toolDiffAdded"),
284
+ ) ?? {
285
+ r: 88,
286
+ g: 173,
287
+ b: 88,
288
+ };
289
+ const removeFg = parseAnsiColorCode(
290
+ readThemeAnsi(theme, "fg", "toolDiffRemoved"),
291
+ ) ?? {
292
+ r: 196,
293
+ g: 98,
294
+ b: 98,
295
+ };
296
+ const addTint = mixRgb(addFg, ADDITION_TINT_TARGET, 0.35);
297
+ const removeTint = mixRgb(removeFg, DELETION_TINT_TARGET, 0.65);
298
+
299
+ const addRowBg = mixRgb(baseBg, addTint, ADD_ROW_BACKGROUND_MIX_RATIO);
300
+ const removeRowBg = mixRgb(
301
+ baseBg,
302
+ removeTint,
303
+ REMOVE_ROW_BACKGROUND_MIX_RATIO,
304
+ );
305
+ const addEmphasisBg = mixRgb(baseBg, addTint, ADD_INLINE_EMPHASIS_MIX_RATIO);
306
+ const removeEmphasisBg = mixRgb(
307
+ baseBg,
308
+ removeTint,
309
+ REMOVE_INLINE_EMPHASIS_MIX_RATIO,
310
+ );
311
+
312
+ return {
313
+ addRowBgAnsi: rgbToBgAnsi(addRowBg),
314
+ removeRowBgAnsi: rgbToBgAnsi(removeRowBg),
315
+ addEmphasisBgAnsi: rgbToBgAnsi(addEmphasisBg),
316
+ removeEmphasisBgAnsi: rgbToBgAnsi(removeEmphasisBg),
317
+ };
318
+ }
319
+
320
+ export function getLineBackground(
321
+ kind: DiffLineKind,
322
+ palette: DiffPalette,
323
+ emphasis: boolean,
324
+ ): string | undefined {
325
+ if (kind === "add") {
326
+ return emphasis ? palette.addEmphasisBgAnsi : palette.addRowBgAnsi;
327
+ }
328
+ if (kind === "remove") {
329
+ return emphasis ? palette.removeEmphasisBgAnsi : palette.removeRowBgAnsi;
330
+ }
331
+ return undefined;
332
+ }
333
+
334
+ function applyBackgroundToVisibleRange(
335
+ ansiText: string,
336
+ start: number,
337
+ end: number,
338
+ backgroundAnsi: string,
339
+ restoreBackgroundAnsi: string,
340
+ ): string {
341
+ if (!ansiText || start >= end || end <= 0) {
342
+ return ansiText;
343
+ }
344
+
345
+ const rangeStart = Math.max(0, start);
346
+ const rangeEnd = Math.max(rangeStart, end);
347
+ let output = "";
348
+ let visibleIndex = 0;
349
+ let index = 0;
350
+ let inRange = false;
351
+
352
+ while (index < ansiText.length) {
353
+ if (ansiText[index] === "\x1b") {
354
+ const sequenceEnd = ansiText.indexOf("m", index);
355
+ if (sequenceEnd !== -1) {
356
+ output += ansiText.slice(index, sequenceEnd + 1);
357
+ index = sequenceEnd + 1;
358
+ continue;
359
+ }
360
+ }
361
+
362
+ if (visibleIndex === rangeStart && !inRange) {
363
+ output += backgroundAnsi;
364
+ inRange = true;
365
+ }
366
+ if (visibleIndex === rangeEnd && inRange) {
367
+ output += restoreBackgroundAnsi;
368
+ inRange = false;
369
+ }
370
+
371
+ output += ansiText[index] ?? "";
372
+ visibleIndex++;
373
+ index++;
374
+ }
375
+
376
+ if (inRange) {
377
+ output += restoreBackgroundAnsi;
378
+ }
379
+
380
+ return output;
381
+ }
382
+
383
+ export function applyInlineSpanHighlight(
384
+ plainText: string,
385
+ renderedText: string,
386
+ spans: DiffSpan[],
387
+ emphasisBgAnsi: string | undefined,
388
+ rowBgAnsi: string | undefined,
389
+ fallbackBgAnsi: string | undefined,
390
+ ): string {
391
+ if (!renderedText || !plainText || spans.length === 0 || !emphasisBgAnsi) {
392
+ return renderedText;
393
+ }
394
+
395
+ const sorted = mergeSpans(
396
+ spans
397
+ .map((span) => ({
398
+ start: Math.max(0, Math.min(plainText.length, span.start)),
399
+ end: Math.max(0, Math.min(plainText.length, span.end)),
400
+ }))
401
+ .filter((span) => span.end > span.start),
402
+ );
403
+ if (sorted.length === 0) {
404
+ return renderedText;
405
+ }
406
+
407
+ const restoreBackgroundAnsi = rowBgAnsi ?? fallbackBgAnsi ?? ANSI_BG_RESET;
408
+ let highlighted = renderedText;
409
+ for (let index = sorted.length - 1; index >= 0; index--) {
410
+ const span = sorted[index];
411
+ if (!span) {
412
+ continue;
413
+ }
414
+ highlighted = applyBackgroundToVisibleRange(
415
+ highlighted,
416
+ span.start,
417
+ span.end,
418
+ emphasisBgAnsi,
419
+ restoreBackgroundAnsi,
420
+ );
421
+ }
422
+
423
+ return highlighted;
424
+ }
425
+
426
+ export function resolveShikiTheme(theme: DiffTheme): string {
427
+ if (process.env.DIFF_THEME) return process.env.DIFF_THEME;
428
+ const background =
429
+ parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolSuccessBg")) ??
430
+ parseAnsiColorCode(readThemeAnsi(theme, "bg", "toolPendingBg")) ??
431
+ parseAnsiColorCode(readThemeAnsi(theme, "bg", "userMessageBg"));
432
+ if (!background) return "github-dark";
433
+ const luminance =
434
+ (background.r * 299 + background.g * 587 + background.b * 114) / 1000;
435
+ return luminance >= 128 ? "github-light" : "github-dark";
436
+ }