@selesai/code 0.10.1 → 0.11.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/CHANGELOG.md +23 -0
- package/dist/config.d.ts +5 -3
- package/dist/config.js +6 -4
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +1 -0
- package/dist/defaults/settings.json +0 -3
- package/dist/extensions/auto-session-name.test.ts +157 -0
- package/dist/extensions/auto-session-name.ts +96 -0
- package/dist/extensions/cost-reconcile.test.ts +76 -0
- package/dist/extensions/cost-reconcile.ts +278 -0
- package/dist/extensions/enable-readonly-tools.test.ts +11 -0
- package/dist/extensions/enable-readonly-tools.ts +7 -3
- package/dist/extensions/package.json +3 -0
- package/dist/extensions/pi-tool-display/CHANGELOG.md +329 -0
- package/dist/extensions/pi-tool-display/LICENSE +21 -0
- package/dist/extensions/pi-tool-display/README.md +417 -0
- package/dist/extensions/pi-tool-display/config/config.example.json +40 -0
- package/dist/extensions/pi-tool-display/index.ts +3 -0
- package/dist/extensions/pi-tool-display/package-lock.json +2555 -0
- package/dist/extensions/pi-tool-display/package.json +86 -0
- package/dist/extensions/pi-tool-display/src/agent-dir.ts +35 -0
- package/dist/extensions/pi-tool-display/src/ansi-utils.ts +116 -0
- package/dist/extensions/pi-tool-display/src/bash-display.ts +213 -0
- package/dist/extensions/pi-tool-display/src/capabilities.ts +88 -0
- package/dist/extensions/pi-tool-display/src/config-modal.ts +541 -0
- package/dist/extensions/pi-tool-display/src/config-store.ts +304 -0
- package/dist/extensions/pi-tool-display/src/debug-logger.ts +150 -0
- package/dist/extensions/pi-tool-display/src/diff-presentation.ts +72 -0
- package/dist/extensions/pi-tool-display/src/diff-renderer.ts +2517 -0
- package/dist/extensions/pi-tool-display/src/disposable.ts +34 -0
- package/dist/extensions/pi-tool-display/src/extension-lifecycle.ts +14 -0
- package/dist/extensions/pi-tool-display/src/index.ts +109 -0
- package/dist/extensions/pi-tool-display/src/line-width-safety.ts +93 -0
- package/dist/extensions/pi-tool-display/src/modal-icons.ts +57 -0
- package/dist/extensions/pi-tool-display/src/pending-diff-preview.ts +370 -0
- package/dist/extensions/pi-tool-display/src/presets.ts +117 -0
- package/dist/extensions/pi-tool-display/src/render-utils.ts +186 -0
- package/dist/extensions/pi-tool-display/src/settings-inspector-modal.ts +475 -0
- package/dist/extensions/pi-tool-display/src/thinking-label.ts +343 -0
- package/dist/extensions/pi-tool-display/src/tool-call-box.ts +181 -0
- package/dist/extensions/pi-tool-display/src/tool-metadata.ts +139 -0
- package/dist/extensions/pi-tool-display/src/tool-overrides.ts +2168 -0
- package/dist/extensions/pi-tool-display/src/types.ts +109 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-markdown.ts +64 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-native.ts +66 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-patch.ts +72 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-renderer.ts +419 -0
- package/dist/extensions/pi-tool-display/src/user-message-box-utils.ts +106 -0
- package/dist/extensions/pi-tool-display/src/write-display-utils.ts +31 -0
- package/dist/extensions/pi-tool-display/src/zellij-modal.ts +1027 -0
- package/dist/extensions/pi-tool-display/tests/active-backlog-red.test.ts +65 -0
- package/dist/extensions/pi-tool-display/tests/ansi-utils.test.ts +345 -0
- package/dist/extensions/pi-tool-display/tests/bash-display.test.ts +442 -0
- package/dist/extensions/pi-tool-display/tests/capabilities-edge.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/capabilities.test.ts +54 -0
- package/dist/extensions/pi-tool-display/tests/config-modal.test.ts +360 -0
- package/dist/extensions/pi-tool-display/tests/config-store.test.ts +87 -0
- package/dist/extensions/pi-tool-display/tests/custom-tool-overrides.test.ts +415 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger-edge.test.ts +630 -0
- package/dist/extensions/pi-tool-display/tests/debug-logger.test.ts +62 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-ansi.test.ts +55 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-edge.test.ts +886 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-numbering.test.ts +135 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-width.test.ts +454 -0
- package/dist/extensions/pi-tool-display/tests/diff-renderer-wrap.test.ts +169 -0
- package/dist/extensions/pi-tool-display/tests/index-integration.test.ts +354 -0
- package/dist/extensions/pi-tool-display/tests/pending-diff-preview.test.ts +79 -0
- package/dist/extensions/pi-tool-display/tests/presets-edge.test.ts +229 -0
- package/dist/extensions/pi-tool-display/tests/presets.test.ts +26 -0
- package/dist/extensions/pi-tool-display/tests/reload-behavior.test.ts +1241 -0
- package/dist/extensions/pi-tool-display/tests/render-utils.test.ts +710 -0
- package/dist/extensions/pi-tool-display/tests/settings-inspector.test.ts +450 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label-edge.test.ts +904 -0
- package/dist/extensions/pi-tool-display/tests/thinking-label.test.ts +72 -0
- package/dist/extensions/pi-tool-display/tests/tool-call-box.test.ts +105 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-config.test.ts +613 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-mcp.test.ts +499 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-registration.test.ts +315 -0
- package/dist/extensions/pi-tool-display/tests/tool-overrides-write-meta.test.ts +68 -0
- package/dist/extensions/pi-tool-display/tests/tool-ui-utils.test.ts +413 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box-performance-red.test.ts +176 -0
- package/dist/extensions/pi-tool-display/tests/user-message-box.test.ts +908 -0
- package/dist/extensions/pi-tool-display/tests/zellij-modal.test.ts +848 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.d.ts +30 -0
- package/dist/extensions/pi-tool-display/tool-display-api-consumer.js +40 -0
- package/dist/extensions/pi-zentui/extensions/zentui/format.ts +46 -1
- package/dist/extensions/pi-zentui/extensions/zentui/index.ts +10 -0
- package/dist/extensions/pi-zentui/extensions/zentui/user-message-styles.ts +1 -1
- package/dist/extensions/pi-zentui/test/extension-compliance.test.ts +3 -3
- package/dist/extensions/pi-zentui/test/format.test.ts +50 -2
- package/dist/modes/interactive/components/startup-box.d.ts +38 -0
- package/dist/modes/interactive/components/startup-box.js +90 -0
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.js +85 -122
- package/dist/skills/agent-browser/SKILL.md +1 -0
- package/dist/skills/batch-grill-me/SKILL.md +1 -0
- package/dist/skills/brandkit/SKILL.md +1 -0
- package/dist/skills/design-references/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend/SKILL.md +1 -0
- package/dist/skills/design-taste-frontend-v1/SKILL.md +1 -0
- package/dist/skills/full-output-enforcement/SKILL.md +1 -0
- package/dist/skills/gpt-taste/SKILL.md +1 -0
- package/dist/skills/grill-me/SKILL.md +1 -0
- package/dist/skills/handoff/SKILL.md +1 -0
- package/dist/skills/handoff-text/SKILL.md +1 -0
- package/dist/skills/high-end-visual-design/SKILL.md +1 -0
- package/dist/skills/image-to-code/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-mobile/SKILL.md +1 -0
- package/dist/skills/imagegen-frontend-web/SKILL.md +1 -0
- package/dist/skills/implanger/SKILL.md +1 -0
- package/dist/skills/improve-codebase/SKILL.md +1 -0
- package/dist/skills/industrial-brutalist-ui/SKILL.md +1 -0
- package/dist/skills/minimalist-ui/SKILL.md +1 -0
- package/dist/skills/pi-subagents/SKILL.md +1 -0
- package/dist/skills/planger/SKILL.md +1 -0
- package/dist/skills/ponytail/SKILL.md +1 -0
- package/dist/skills/ponytail-audit/SKILL.md +1 -0
- package/dist/skills/ponytail-debt/SKILL.md +1 -0
- package/dist/skills/ponytail-gain/SKILL.md +1 -0
- package/dist/skills/ponytail-help/SKILL.md +1 -0
- package/dist/skills/ponytail-review/SKILL.md +1 -0
- package/dist/skills/redesign-existing-projects/SKILL.md +1 -0
- package/dist/skills/selesai-handoff/SKILL.md +1 -0
- package/dist/skills/stitch-design-taste/SKILL.md +1 -0
- package/dist/skills/unlazy/SKILL.md +1 -0
- package/dist/skills/web-design-guidelines/SKILL.md +1 -0
- package/dist/skills/workflow/SKILL.md +1 -0
- package/package.json +3 -3
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Box, type Component } from "@earendil-works/pi-tui";
|
|
4
|
+
import { renderEditDiffResult } from "../src/diff-renderer.ts";
|
|
5
|
+
|
|
6
|
+
const theme = {
|
|
7
|
+
fg: (_color: string, text: string): string => text,
|
|
8
|
+
bold: (text: string): string => text,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function stripAnsi(text: string): string {
|
|
12
|
+
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function renderInsideToolBox(component: Component, width: number): string[] {
|
|
16
|
+
const box = new Box(1, 1);
|
|
17
|
+
box.addChild(component);
|
|
18
|
+
return box.render(width).map(stripAnsi);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractDisplayedLineNumber(line: string): string | null {
|
|
22
|
+
const match = line.match(/^\s*(?:▌\s+)?(\S*)\s+│/);
|
|
23
|
+
if (!match) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
return match[1] ?? "";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function splitRenderedRow(line: string, width: number): { left: string; right: string } {
|
|
30
|
+
const separatorWidth = 3;
|
|
31
|
+
const leftWidth = Math.floor((width - separatorWidth) / 2);
|
|
32
|
+
return {
|
|
33
|
+
left: line.slice(0, leftWidth),
|
|
34
|
+
right: line.slice(leftWidth + separatorWidth),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractSplitDiffRows(lines: string[], width: number): Array<{ left: string; right: string }> {
|
|
39
|
+
return lines
|
|
40
|
+
.map((line) => splitRenderedRow(line, width))
|
|
41
|
+
.filter(({ left, right }) => extractDisplayedLineNumber(left) !== null || extractDisplayedLineNumber(right) !== null);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("split diff derives sequential new-side line numbers for canonical numbered diffs", () => {
|
|
45
|
+
const width = 120;
|
|
46
|
+
const component = renderEditDiffResult(
|
|
47
|
+
{
|
|
48
|
+
diff: [
|
|
49
|
+
" 6|export default function toolDisplay() {",
|
|
50
|
+
"- 7| const oldA = true;",
|
|
51
|
+
"- 8| const oldB = true;",
|
|
52
|
+
" 9| setup();",
|
|
53
|
+
"-10| const legacy = getLegacyMode();",
|
|
54
|
+
"+10| if (ready) {",
|
|
55
|
+
" 11| run();",
|
|
56
|
+
"+11| start();",
|
|
57
|
+
" 12| cleanup();",
|
|
58
|
+
"+12| }",
|
|
59
|
+
" 13| finalize();",
|
|
60
|
+
"+14| return true;",
|
|
61
|
+
" 14|}",
|
|
62
|
+
].join("\n"),
|
|
63
|
+
},
|
|
64
|
+
{ expanded: true, filePath: "demo.ts" },
|
|
65
|
+
{
|
|
66
|
+
diffViewMode: "split",
|
|
67
|
+
diffSplitMinWidth: 80,
|
|
68
|
+
diffCollapsedLines: 24,
|
|
69
|
+
diffWordWrap: false,
|
|
70
|
+
} as any,
|
|
71
|
+
theme,
|
|
72
|
+
"",
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const lines = renderInsideToolBox(component, width);
|
|
76
|
+
const diffRows = extractSplitDiffRows(lines, width);
|
|
77
|
+
const rightNumbers = diffRows
|
|
78
|
+
.map(({ right }) => extractDisplayedLineNumber(right))
|
|
79
|
+
.filter((value): value is string => value !== null && /^\d+$/.test(value));
|
|
80
|
+
|
|
81
|
+
assert.deepStrictEqual(
|
|
82
|
+
rightNumbers,
|
|
83
|
+
["6", "7", "8", "9", "10", "11", "12", "13", "14", "15"],
|
|
84
|
+
`Expected the new pane to preserve a strictly sequential numbering model.\n${lines.join("\n")}`,
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const hashlineDiff = [
|
|
89
|
+
" 1#ZP:alpha",
|
|
90
|
+
"-2# :beta",
|
|
91
|
+
"+2#A1:bravo",
|
|
92
|
+
" 3#BC:gamma",
|
|
93
|
+
].join("\n");
|
|
94
|
+
|
|
95
|
+
function renderHashlineDiff(expanded: boolean): string {
|
|
96
|
+
const width = 100;
|
|
97
|
+
const component = renderEditDiffResult(
|
|
98
|
+
{ diff: hashlineDiff },
|
|
99
|
+
{ expanded, filePath: "demo.txt" },
|
|
100
|
+
{
|
|
101
|
+
diffViewMode: "unified",
|
|
102
|
+
diffSplitMinWidth: 80,
|
|
103
|
+
diffCollapsedLines: 24,
|
|
104
|
+
diffWordWrap: false,
|
|
105
|
+
} as any,
|
|
106
|
+
theme,
|
|
107
|
+
"",
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return renderInsideToolBox(component, width).join("\n");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
test("collapsed edit diff renderer hides hashline anchors while preserving visible line numbers", () => {
|
|
114
|
+
const rendered = renderHashlineDiff(false);
|
|
115
|
+
|
|
116
|
+
assert.doesNotMatch(
|
|
117
|
+
rendered,
|
|
118
|
+
/\b\d+#(?:ZP|A1|BC|\s{2}):/,
|
|
119
|
+
`Expected hashline anchor metadata to be hidden from collapsed edit diff display.\n${rendered}`,
|
|
120
|
+
);
|
|
121
|
+
assert.match(rendered, /\b1\s+│ alpha\b/);
|
|
122
|
+
assert.match(rendered, /\b2\s+│ bravo\b/);
|
|
123
|
+
assert.match(rendered, /\b3\s+│ gamma\b/);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("expanded edit diff renderer shows compact hashline labels in the line-number gutter", () => {
|
|
127
|
+
const rendered = renderHashlineDiff(true);
|
|
128
|
+
|
|
129
|
+
assert.match(rendered, /(?:^|\n) 1#ZP│ alpha\b/);
|
|
130
|
+
assert.match(rendered, /(?:^|\n) 2# {2}│ beta\b/);
|
|
131
|
+
assert.match(rendered, /(?:^|\n) 2#A1│ bravo\b/);
|
|
132
|
+
assert.match(rendered, /(?:^|\n) 3#BC│ gamma\b/);
|
|
133
|
+
assert.doesNotMatch(rendered, /(?:^|\n) {2,}1#ZP│/);
|
|
134
|
+
assert.doesNotMatch(rendered, /│\s+\d+#(?:ZP|A1|BC|\s{2}):/);
|
|
135
|
+
});
|
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { Box, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
4
|
+
import { buildDiffSummaryText, resolveDiffPresentationMode } from "../src/diff-presentation.ts";
|
|
5
|
+
import { renderEditDiffResult, renderWriteDiffResult } from "../src/diff-renderer.ts";
|
|
6
|
+
|
|
7
|
+
const diffConfig = {
|
|
8
|
+
diffViewMode: "auto" as const,
|
|
9
|
+
diffSplitMinWidth: 80,
|
|
10
|
+
diffCollapsedLines: 24,
|
|
11
|
+
diffWordWrap: true,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const theme = {
|
|
15
|
+
fg: (_color: string, text: string): string => text,
|
|
16
|
+
bold: (text: string): string => text,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
interface RgbColor {
|
|
20
|
+
r: number;
|
|
21
|
+
g: number;
|
|
22
|
+
b: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const ADDITION_TINT_TARGET: RgbColor = { r: 84, g: 190, b: 118 };
|
|
26
|
+
const DELETION_TINT_TARGET: RgbColor = { r: 232, g: 95, b: 122 };
|
|
27
|
+
const ADD_ROW_BACKGROUND_MIX_RATIO = 0.12;
|
|
28
|
+
const REMOVE_ROW_BACKGROUND_MIX_RATIO = 0.12;
|
|
29
|
+
const ADD_INLINE_EMPHASIS_MIX_RATIO = 0.26;
|
|
30
|
+
|
|
31
|
+
function mixRgb(base: RgbColor, tint: RgbColor, ratio: number): RgbColor {
|
|
32
|
+
const clamped = Math.max(0, Math.min(1, ratio));
|
|
33
|
+
return {
|
|
34
|
+
r: base.r * (1 - clamped) + tint.r * clamped,
|
|
35
|
+
g: base.g * (1 - clamped) + tint.g * clamped,
|
|
36
|
+
b: base.b * (1 - clamped) + tint.b * clamped,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function rgbToBgAnsi(color: RgbColor): string {
|
|
41
|
+
return `\x1b[48;2;${Math.round(color.r)};${Math.round(color.g)};${Math.round(color.b)}m`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveInlineHighlightPalette(baseBg: RgbColor, addFg: RgbColor, removeFg: RgbColor): {
|
|
45
|
+
addRowBg: string;
|
|
46
|
+
removeRowBg: string;
|
|
47
|
+
addEmphasisBg: string;
|
|
48
|
+
} {
|
|
49
|
+
const addTint = mixRgb(addFg, ADDITION_TINT_TARGET, 0.35);
|
|
50
|
+
const removeTint = mixRgb(removeFg, DELETION_TINT_TARGET, 0.65);
|
|
51
|
+
return {
|
|
52
|
+
addRowBg: rgbToBgAnsi(mixRgb(baseBg, addTint, ADD_ROW_BACKGROUND_MIX_RATIO)),
|
|
53
|
+
removeRowBg: rgbToBgAnsi(mixRgb(baseBg, removeTint, REMOVE_ROW_BACKGROUND_MIX_RATIO)),
|
|
54
|
+
addEmphasisBg: rgbToBgAnsi(mixRgb(baseBg, addTint, ADD_INLINE_EMPHASIS_MIX_RATIO)),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function renderInsideToolBox(component: Component, width: number): string[] {
|
|
59
|
+
const box = new Box(1, 1);
|
|
60
|
+
box.addChild(component);
|
|
61
|
+
return box.render(width);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function renderInsideThemedToolBox(component: Component, width: number, background: RgbColor): string[] {
|
|
65
|
+
const backgroundAnsi = rgbToBgAnsi(background);
|
|
66
|
+
const box = new Box(1, 1, (text: string) => `${backgroundAnsi}${text}\x1b[0m`);
|
|
67
|
+
box.addChild(component);
|
|
68
|
+
return box.render(width);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function assertLinesFitWidth(lines: string[], width: number): void {
|
|
72
|
+
for (const line of lines) {
|
|
73
|
+
assert.ok(
|
|
74
|
+
visibleWidth(line) <= width,
|
|
75
|
+
`rendered line exceeded width ${width}: ${visibleWidth(line)} :: ${JSON.stringify(line)}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface VisibleBackgroundCell {
|
|
81
|
+
char: string;
|
|
82
|
+
background: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isFiniteSgrParam(value: number | undefined): value is number {
|
|
86
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function readSgrBackgroundSequence(params: number[], index: number): string | undefined {
|
|
90
|
+
if (params[index] !== 48) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const colorMode = params[index + 1];
|
|
95
|
+
if (colorMode === 5) {
|
|
96
|
+
const colorValue = params[index + 2];
|
|
97
|
+
return isFiniteSgrParam(colorValue) ? `\x1b[48;5;${colorValue}m` : undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (colorMode === 2) {
|
|
101
|
+
const red = params[index + 2];
|
|
102
|
+
const green = params[index + 3];
|
|
103
|
+
const blue = params[index + 4];
|
|
104
|
+
return isFiniteSgrParam(red) && isFiniteSgrParam(green) && isFiniteSgrParam(blue)
|
|
105
|
+
? `\x1b[48;2;${red};${green};${blue}m`
|
|
106
|
+
: undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function updateBackgroundState(rawParams: string, currentBackground: string | null): string | null {
|
|
113
|
+
if (!rawParams.trim()) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const params = rawParams
|
|
118
|
+
.split(";")
|
|
119
|
+
.map((token) => Number.parseInt(token, 10))
|
|
120
|
+
.filter((value) => Number.isFinite(value));
|
|
121
|
+
let nextBackground = currentBackground;
|
|
122
|
+
|
|
123
|
+
for (let index = 0; index < params.length; index += 1) {
|
|
124
|
+
const param = params[index] ?? 0;
|
|
125
|
+
if (param === 0 || param === 49) {
|
|
126
|
+
nextBackground = null;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if ((param >= 40 && param <= 47) || (param >= 100 && param <= 107)) {
|
|
130
|
+
nextBackground = `\x1b[${param}m`;
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const backgroundSequence = readSgrBackgroundSequence(params, index);
|
|
135
|
+
if (backgroundSequence) {
|
|
136
|
+
nextBackground = backgroundSequence;
|
|
137
|
+
index += backgroundSequence.includes("48;5;") ? 2 : 4;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return nextBackground;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function collectVisibleBackgrounds(text: string): VisibleBackgroundCell[] {
|
|
145
|
+
const cells: VisibleBackgroundCell[] = [];
|
|
146
|
+
let activeBackground: string | null = null;
|
|
147
|
+
|
|
148
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
149
|
+
if (text[index] === "\x1b" && text[index + 1] === "[") {
|
|
150
|
+
const sequenceEnd = text.indexOf("m", index);
|
|
151
|
+
if (sequenceEnd !== -1) {
|
|
152
|
+
activeBackground = updateBackgroundState(text.slice(index + 2, sequenceEnd), activeBackground);
|
|
153
|
+
index = sequenceEnd;
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
cells.push({ char: text[index] ?? "", background: activeBackground });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return cells;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function assertEveryVisibleCellHasBackground(line: string, label: string): void {
|
|
165
|
+
const visibleCells = collectVisibleBackgrounds(line);
|
|
166
|
+
assert.ok(visibleCells.length > 0, `expected visible cells for ${label}`);
|
|
167
|
+
assert.ok(
|
|
168
|
+
visibleCells.every((cell) => cell.background !== null),
|
|
169
|
+
`${label} leaked terminal background: ${JSON.stringify(line)}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
test("diff presentation mode progressively degrades for narrow widths", () => {
|
|
174
|
+
assert.equal(resolveDiffPresentationMode(diffConfig, 120, true), "split");
|
|
175
|
+
assert.equal(resolveDiffPresentationMode(diffConfig, 24, false), "unified");
|
|
176
|
+
assert.equal(resolveDiffPresentationMode(diffConfig, 12, false), "compact");
|
|
177
|
+
assert.equal(resolveDiffPresentationMode(diffConfig, 7, false), "summary");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("diff summary text always fits the available width", () => {
|
|
181
|
+
for (const width of [1, 4, 7, 12, 24]) {
|
|
182
|
+
const summary = buildDiffSummaryText(
|
|
183
|
+
{ added: 12, removed: 3, hunks: 2, files: 1 },
|
|
184
|
+
width,
|
|
185
|
+
);
|
|
186
|
+
assert.ok(visibleWidth(summary) <= width);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("edit diff renderer respects parent box width across narrow layouts", () => {
|
|
191
|
+
const component = renderEditDiffResult(
|
|
192
|
+
{
|
|
193
|
+
diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1,2 +1,2 @@\n-old value\n+new value\n unchanged\n",
|
|
194
|
+
},
|
|
195
|
+
{ expanded: false, filePath: "demo.txt" },
|
|
196
|
+
diffConfig as any,
|
|
197
|
+
theme,
|
|
198
|
+
"",
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
for (const width of [23, 17, 7]) {
|
|
202
|
+
const lines = renderInsideToolBox(component, width);
|
|
203
|
+
assertLinesFitWidth(lines, width);
|
|
204
|
+
assert.ok(lines.some((line) => visibleWidth(line) > 0));
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("write diff renderer respects parent box width across narrow layouts", () => {
|
|
209
|
+
const component = renderWriteDiffResult(
|
|
210
|
+
"hello world\nsecond line\n",
|
|
211
|
+
{ expanded: false, filePath: "demo.txt", fileExistedBeforeWrite: false },
|
|
212
|
+
diffConfig as any,
|
|
213
|
+
theme,
|
|
214
|
+
"",
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
for (const width of [23, 17, 7]) {
|
|
218
|
+
const lines = renderInsideToolBox(component, width);
|
|
219
|
+
assertLinesFitWidth(lines, width);
|
|
220
|
+
assert.ok(lines.some((line) => visibleWidth(line) > 0));
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("write overwrite diff renderer falls back when the overwrite matrix would be too large", () => {
|
|
225
|
+
const previousContent = `${Array.from({ length: 1100 }, (_, index) => `old-${index}`).join("\n")}\n`;
|
|
226
|
+
const nextContent = `${Array.from({ length: 1100 }, (_, index) => `new-${index}`).join("\n")}\n`;
|
|
227
|
+
const component = renderWriteDiffResult(
|
|
228
|
+
nextContent,
|
|
229
|
+
{
|
|
230
|
+
expanded: false,
|
|
231
|
+
filePath: "demo.txt",
|
|
232
|
+
fileExistedBeforeWrite: true,
|
|
233
|
+
previousContent,
|
|
234
|
+
},
|
|
235
|
+
diffConfig as any,
|
|
236
|
+
theme,
|
|
237
|
+
"",
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const lines = renderInsideToolBox(component, 80);
|
|
241
|
+
assertLinesFitWidth(lines, 80);
|
|
242
|
+
assert.match(lines.join("\n"), /overwrite diff omitted/i);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("split diff renderer preserves full background coverage inside the default tool shell", () => {
|
|
246
|
+
const baseBg = { r: 10, g: 20, b: 30 };
|
|
247
|
+
const addFg = { r: 100, g: 150, b: 200 };
|
|
248
|
+
const removeFg = { r: 200, g: 100, b: 120 };
|
|
249
|
+
const splitDiffConfig = { ...diffConfig, diffViewMode: "split" };
|
|
250
|
+
const ansiTheme = {
|
|
251
|
+
fg: (_color: string, text: string): string => `\x1b[38;2;1;2;3m${text}\x1b[0m`,
|
|
252
|
+
bold: (text: string): string => text,
|
|
253
|
+
getFgAnsi: (slot: string): string | undefined => {
|
|
254
|
+
if (slot === "toolDiffAdded") {
|
|
255
|
+
return `\x1b[38;2;${addFg.r};${addFg.g};${addFg.b}m`;
|
|
256
|
+
}
|
|
257
|
+
if (slot === "toolDiffRemoved") {
|
|
258
|
+
return `\x1b[38;2;${removeFg.r};${removeFg.g};${removeFg.b}m`;
|
|
259
|
+
}
|
|
260
|
+
if (slot === "dim") {
|
|
261
|
+
return "\x1b[38;5;8m";
|
|
262
|
+
}
|
|
263
|
+
return undefined;
|
|
264
|
+
},
|
|
265
|
+
getBgAnsi: (slot: string): string | undefined => {
|
|
266
|
+
if (slot === "toolSuccessBg") {
|
|
267
|
+
return `\x1b[48;2;${baseBg.r};${baseBg.g};${baseBg.b}m`;
|
|
268
|
+
}
|
|
269
|
+
return undefined;
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
const component = renderEditDiffResult(
|
|
273
|
+
{
|
|
274
|
+
diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1,2 +1,3 @@\n same value\n-old value\n+new value\n+another line\n",
|
|
275
|
+
},
|
|
276
|
+
{ expanded: true, filePath: "demo.txt" },
|
|
277
|
+
splitDiffConfig as any,
|
|
278
|
+
ansiTheme,
|
|
279
|
+
"",
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const lines = renderInsideThemedToolBox(component, 120, baseBg);
|
|
283
|
+
assertLinesFitWidth(lines, 120);
|
|
284
|
+
|
|
285
|
+
const summaryLine = lines.find((line) => line.includes("diff") && line.includes("+2"));
|
|
286
|
+
assert.ok(summaryLine, `expected split diff summary line:\n${lines.join("\n")}`);
|
|
287
|
+
assertEveryVisibleCellHasBackground(summaryLine, "split diff summary line");
|
|
288
|
+
|
|
289
|
+
const headerLine = lines.find((line) => line.includes("old") && line.includes("new"));
|
|
290
|
+
assert.ok(headerLine, `expected split diff header line:\n${lines.join("\n")}`);
|
|
291
|
+
assertEveryVisibleCellHasBackground(headerLine, "split diff header line");
|
|
292
|
+
|
|
293
|
+
const contextLine = lines.find((line) => line.includes("same value"));
|
|
294
|
+
assert.ok(contextLine, `expected split diff context line:\n${lines.join("\n")}`);
|
|
295
|
+
assertEveryVisibleCellHasBackground(contextLine, "split diff context line");
|
|
296
|
+
|
|
297
|
+
const blankCompanionLine = lines.find((line) => line.includes("another line"));
|
|
298
|
+
assert.ok(blankCompanionLine, `expected split diff blank companion line:\n${lines.join("\n")}`);
|
|
299
|
+
assertEveryVisibleCellHasBackground(blankCompanionLine, "split diff blank companion line");
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("split diff falls back to theme.bg when the default tool shell provides the outer background", () => {
|
|
303
|
+
const baseBg = { r: 16, g: 24, b: 32 };
|
|
304
|
+
const addFg = { r: 90, g: 180, b: 120 };
|
|
305
|
+
const removeFg = { r: 210, g: 120, b: 140 };
|
|
306
|
+
const component = renderEditDiffResult(
|
|
307
|
+
{
|
|
308
|
+
diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1,2 +1,3 @@\n same value\n-old value\n+new value\n+another line\n",
|
|
309
|
+
},
|
|
310
|
+
{ expanded: true, filePath: "demo.txt" },
|
|
311
|
+
{ ...diffConfig, diffViewMode: "split" } as any,
|
|
312
|
+
{
|
|
313
|
+
fg: (_color: string, text: string): string => `\x1b[38;2;1;2;3m${text}\x1b[0m`,
|
|
314
|
+
bg: (slot: string, text: string): string => {
|
|
315
|
+
if (slot === "toolSuccessBg") {
|
|
316
|
+
return `\x1b[48;2;${baseBg.r};${baseBg.g};${baseBg.b}m${text}\x1b[0m`;
|
|
317
|
+
}
|
|
318
|
+
return text;
|
|
319
|
+
},
|
|
320
|
+
bold: (text: string): string => text,
|
|
321
|
+
getFgAnsi: (slot: string): string | undefined => {
|
|
322
|
+
if (slot === "toolDiffAdded") {
|
|
323
|
+
return `\x1b[38;2;${addFg.r};${addFg.g};${addFg.b}m`;
|
|
324
|
+
}
|
|
325
|
+
if (slot === "toolDiffRemoved") {
|
|
326
|
+
return `\x1b[38;2;${removeFg.r};${removeFg.g};${removeFg.b}m`;
|
|
327
|
+
}
|
|
328
|
+
if (slot === "dim") {
|
|
329
|
+
return "\x1b[38;5;8m";
|
|
330
|
+
}
|
|
331
|
+
return undefined;
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
"",
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
const lines = renderInsideThemedToolBox(component, 120, baseBg);
|
|
338
|
+
assertLinesFitWidth(lines, 120);
|
|
339
|
+
|
|
340
|
+
for (const [index, line] of lines.entries()) {
|
|
341
|
+
if (visibleWidth(line) === 0) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
assertEveryVisibleCellHasBackground(line, `theme.bg fallback line ${index}`);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("row backgrounds keep trailing padding painted to the rendered width", () => {
|
|
349
|
+
const baseBg = { r: 10, g: 20, b: 30 };
|
|
350
|
+
const addFg = { r: 100, g: 150, b: 200 };
|
|
351
|
+
const removeFg = { r: 200, g: 100, b: 120 };
|
|
352
|
+
const palette = resolveInlineHighlightPalette(baseBg, addFg, removeFg);
|
|
353
|
+
const ansiTheme = {
|
|
354
|
+
fg: (_color: string, text: string): string => `\x1b[38;2;1;2;3m${text}\x1b[0m`,
|
|
355
|
+
bold: (text: string): string => text,
|
|
356
|
+
getFgAnsi: (slot: string): string | undefined => {
|
|
357
|
+
if (slot === "toolDiffAdded") {
|
|
358
|
+
return `\x1b[38;2;${addFg.r};${addFg.g};${addFg.b}m`;
|
|
359
|
+
}
|
|
360
|
+
if (slot === "toolDiffRemoved") {
|
|
361
|
+
return `\x1b[38;2;${removeFg.r};${removeFg.g};${removeFg.b}m`;
|
|
362
|
+
}
|
|
363
|
+
return undefined;
|
|
364
|
+
},
|
|
365
|
+
getBgAnsi: (slot: string): string | undefined => {
|
|
366
|
+
if (slot === "toolSuccessBg") {
|
|
367
|
+
return `\x1b[48;2;${baseBg.r};${baseBg.g};${baseBg.b}m`;
|
|
368
|
+
}
|
|
369
|
+
return undefined;
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
const component = renderEditDiffResult(
|
|
373
|
+
{
|
|
374
|
+
diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1 +1 @@\n-keep before\n+keep after\n",
|
|
375
|
+
},
|
|
376
|
+
{ expanded: true, filePath: "demo.txt" },
|
|
377
|
+
diffConfig as any,
|
|
378
|
+
ansiTheme,
|
|
379
|
+
"",
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
const lines = component.render(60);
|
|
383
|
+
const addedLine = lines.find((line) => line.includes("after"));
|
|
384
|
+
assert.ok(addedLine, "expected an added line containing the diff content");
|
|
385
|
+
assert.equal(visibleWidth(addedLine), 60, `expected rendered line width 60: ${JSON.stringify(addedLine)}`);
|
|
386
|
+
|
|
387
|
+
const visibleCells = collectVisibleBackgrounds(addedLine);
|
|
388
|
+
const lastNonSpaceIndex = visibleCells.findLastIndex((cell) => cell.char.trim().length > 0);
|
|
389
|
+
assert.ok(lastNonSpaceIndex >= 0, `expected at least one non-space cell: ${JSON.stringify(addedLine)}`);
|
|
390
|
+
const trailingCells = visibleCells.slice(lastNonSpaceIndex + 1);
|
|
391
|
+
assert.ok(trailingCells.length > 0, `expected trailing padding after the visible text: ${JSON.stringify(addedLine)}`);
|
|
392
|
+
assert.ok(
|
|
393
|
+
trailingCells.every((cell) => cell.char === " "),
|
|
394
|
+
`expected trailing cells to be spaces only: ${JSON.stringify(visibleCells.slice(Math.max(0, lastNonSpaceIndex - 4)))}`,
|
|
395
|
+
);
|
|
396
|
+
assert.ok(
|
|
397
|
+
trailingCells.every((cell) => cell.background === palette.addRowBg),
|
|
398
|
+
`expected trailing padding to keep the row background active: ${JSON.stringify(trailingCells)}`,
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test("inline emphasis backgrounds remain visible while row backgrounds still recover after resets", () => {
|
|
403
|
+
const baseBg = { r: 10, g: 20, b: 30 };
|
|
404
|
+
const addFg = { r: 100, g: 150, b: 200 };
|
|
405
|
+
const removeFg = { r: 200, g: 100, b: 120 };
|
|
406
|
+
const palette = resolveInlineHighlightPalette(baseBg, addFg, removeFg);
|
|
407
|
+
const splitDiffConfig = { ...diffConfig, diffViewMode: "split" };
|
|
408
|
+
const ansiTheme = {
|
|
409
|
+
fg: (_color: string, text: string): string => `\x1b[38;2;1;2;3m${text}\x1b[0m`,
|
|
410
|
+
bold: (text: string): string => text,
|
|
411
|
+
getFgAnsi: (slot: string): string | undefined => {
|
|
412
|
+
if (slot === "toolDiffAdded") {
|
|
413
|
+
return `\x1b[38;2;${addFg.r};${addFg.g};${addFg.b}m`;
|
|
414
|
+
}
|
|
415
|
+
if (slot === "toolDiffRemoved") {
|
|
416
|
+
return `\x1b[38;2;${removeFg.r};${removeFg.g};${removeFg.b}m`;
|
|
417
|
+
}
|
|
418
|
+
return undefined;
|
|
419
|
+
},
|
|
420
|
+
getBgAnsi: (slot: string): string | undefined => {
|
|
421
|
+
if (slot === "toolSuccessBg") {
|
|
422
|
+
return `\x1b[48;2;${baseBg.r};${baseBg.g};${baseBg.b}m`;
|
|
423
|
+
}
|
|
424
|
+
return undefined;
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
const component = renderEditDiffResult(
|
|
428
|
+
{
|
|
429
|
+
diff: "--- a/demo.txt\n+++ b/demo.txt\n@@ -1 +1 @@\n-keep before suffix\n+keep after suffix\n",
|
|
430
|
+
},
|
|
431
|
+
{ expanded: true, filePath: "demo.txt" },
|
|
432
|
+
splitDiffConfig as any,
|
|
433
|
+
ansiTheme,
|
|
434
|
+
"",
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
const lines = renderInsideToolBox(component, 120);
|
|
438
|
+
const addedLine = lines.find((line) => line.includes("after"));
|
|
439
|
+
assert.ok(addedLine, "expected an added line containing the inline-emphasized text");
|
|
440
|
+
assert.ok(
|
|
441
|
+
addedLine.includes(`${palette.addEmphasisBg}after${palette.addRowBg}`),
|
|
442
|
+
`expected inline emphasis background to remain active for the changed span: ${JSON.stringify(addedLine)}`,
|
|
443
|
+
);
|
|
444
|
+
assert.ok(
|
|
445
|
+
!addedLine.includes(`${palette.addEmphasisBg}${palette.addRowBg}after`),
|
|
446
|
+
`expected row background not to overwrite the inline emphasis span immediately: ${JSON.stringify(addedLine)}`,
|
|
447
|
+
);
|
|
448
|
+
const stabilizedResetAnsi = "\x1b[39;22;23;24;25;27;28;29;59m";
|
|
449
|
+
assert.ok(
|
|
450
|
+
addedLine.includes(`${stabilizedResetAnsi}${palette.addRowBg}`)
|
|
451
|
+
|| addedLine.includes(`${stabilizedResetAnsi}${palette.removeRowBg}`),
|
|
452
|
+
`expected row background to be restored after reset sequences: ${JSON.stringify(addedLine)}`,
|
|
453
|
+
);
|
|
454
|
+
});
|