@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,65 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { Box, type Component } from "@earendil-works/pi-tui";
|
|
5
|
+
import { renderEditDiffResult } from "../src/diff-renderer.ts";
|
|
6
|
+
|
|
7
|
+
const theme = {
|
|
8
|
+
fg: (_color: string, text: string): string => text,
|
|
9
|
+
bold: (text: string): string => text,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const diffConfig = {
|
|
13
|
+
diffViewMode: "auto" as const,
|
|
14
|
+
diffSplitMinWidth: 80,
|
|
15
|
+
diffCollapsedLines: 24,
|
|
16
|
+
diffWordWrap: false,
|
|
17
|
+
diffIndicatorMode: "bars" as const,
|
|
18
|
+
expandedPreviewMaxLines: 32,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function renderInsideToolBox(component: Component, width: number): string[] {
|
|
22
|
+
const box = new Box(1, 1);
|
|
23
|
+
box.addChild(component);
|
|
24
|
+
return box.render(width);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function buildLargeUnifiedDiff(changeCount: number): string {
|
|
28
|
+
const lines = [
|
|
29
|
+
"--- a/large.txt",
|
|
30
|
+
"+++ b/large.txt",
|
|
31
|
+
`@@ -1,${changeCount} +1,${changeCount} @@`,
|
|
32
|
+
];
|
|
33
|
+
for (let lineNumber = 1; lineNumber <= changeCount; lineNumber++) {
|
|
34
|
+
lines.push(`-old line ${lineNumber}`);
|
|
35
|
+
lines.push(`+new line ${lineNumber}`);
|
|
36
|
+
}
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
test("issue #23: expanded large diffs stay bounded for small tmux panes", () => {
|
|
41
|
+
const component = renderEditDiffResult(
|
|
42
|
+
{ diff: buildLargeUnifiedDiff(80) },
|
|
43
|
+
{ expanded: true, filePath: "large.txt" },
|
|
44
|
+
diffConfig as any,
|
|
45
|
+
theme,
|
|
46
|
+
"",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const lines = renderInsideToolBox(component, 100);
|
|
50
|
+
assert.ok(
|
|
51
|
+
lines.length <= diffConfig.expandedPreviewMaxLines + 8,
|
|
52
|
+
`expected expanded large diff to stay bounded near ${diffConfig.expandedPreviewMaxLines} lines, rendered ${lines.length}`,
|
|
53
|
+
);
|
|
54
|
+
assert.ok(
|
|
55
|
+
lines.some((line) => /remaining|omitted|collapsed|more/i.test(line)),
|
|
56
|
+
"expected a visible truncation hint for omitted large-diff content",
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("PR #24: lockfile uses patched esbuild 0.28.1", () => {
|
|
61
|
+
const lockfile = JSON.parse(readFileSync(new URL("../package-lock.json", import.meta.url), "utf8")) as {
|
|
62
|
+
packages?: Record<string, { version?: string }>;
|
|
63
|
+
};
|
|
64
|
+
assert.equal(lockfile.packages?.["node_modules/esbuild"]?.version, "0.28.1");
|
|
65
|
+
});
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
ANSI_SGR_PATTERN,
|
|
5
|
+
STYLE_RESET_PARAMS,
|
|
6
|
+
toSgrParams,
|
|
7
|
+
stripBackgroundSgrParams,
|
|
8
|
+
sanitizeAnsiForThemedOutput,
|
|
9
|
+
} from "../src/ansi-utils.ts";
|
|
10
|
+
|
|
11
|
+
// ─── toSgrParams ───────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
test("toSgrParams returns [0] for empty string input", () => {
|
|
14
|
+
assert.deepEqual(toSgrParams(""), [0]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("toSgrParams returns [0] for whitespace-only input", () => {
|
|
18
|
+
assert.deepEqual(toSgrParams(" "), [0]);
|
|
19
|
+
assert.deepEqual(toSgrParams("\t"), [0]);
|
|
20
|
+
assert.deepEqual(toSgrParams("\n"), [0]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("toSgrParams returns [0] for single zero", () => {
|
|
24
|
+
assert.deepEqual(toSgrParams("0"), [0]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("toSgrParams parses semicolon-separated numeric tokens", () => {
|
|
28
|
+
assert.deepEqual(toSgrParams("1;2;3"), [1, 2, 3]);
|
|
29
|
+
assert.deepEqual(toSgrParams("38;5;196"), [38, 5, 196]);
|
|
30
|
+
assert.deepEqual(toSgrParams("48;2;12;49;200"), [48, 2, 12, 49, 200]);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("toSgrParams filters out non-numeric tokens", () => {
|
|
34
|
+
assert.deepEqual(toSgrParams("1;abc;3"), [1, 3]);
|
|
35
|
+
assert.deepEqual(toSgrParams("hello;world"), []);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("toSgrParams filters out NaN from empty segments", () => {
|
|
39
|
+
// "1;;3".split(";") => ["1", "", "3"] => parseInt => [1, NaN, 3] => filter(isFinite) => [1, 3]
|
|
40
|
+
assert.deepEqual(toSgrParams("1;;3"), [1, 3]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("toSgrParams handles leading/trailing semicolons", () => {
|
|
44
|
+
// ";1;2" => ["", "1", "2"] => [NaN, 1, 2] => filter => [1, 2]
|
|
45
|
+
assert.deepEqual(toSgrParams(";1;2"), [1, 2]);
|
|
46
|
+
// "1;2;" => ["1", "2", ""] => [1, 2, NaN] => [1, 2]
|
|
47
|
+
assert.deepEqual(toSgrParams("1;2;"), [1, 2]);
|
|
48
|
+
// ";" => ["", ""] => [NaN, NaN] => []
|
|
49
|
+
assert.deepEqual(toSgrParams(";"), []);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("toSgrParams handles negative values", () => {
|
|
53
|
+
assert.deepEqual(toSgrParams("-1;2"), [-1, 2]);
|
|
54
|
+
assert.deepEqual(toSgrParams("1;-2;3"), [1, -2, 3]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("toSgrParams handles very large numbers", () => {
|
|
58
|
+
assert.deepEqual(toSgrParams("9999999999"), [9999999999]);
|
|
59
|
+
assert.deepEqual(toSgrParams("1;9999999999;3"), [1, 9999999999, 3]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("toSgrParams returns [] for semicolons without valid numbers", () => {
|
|
63
|
+
// ";abc" => ["", "abc"] => [NaN, NaN] => []
|
|
64
|
+
assert.deepEqual(toSgrParams(";abc"), []);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("toSgrParams handles single param", () => {
|
|
68
|
+
assert.deepEqual(toSgrParams("42"), [42]);
|
|
69
|
+
assert.deepEqual(toSgrParams("0"), [0]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ─── stripBackgroundSgrParams ──────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
test("stripBackgroundSgrParams strips param 49 (background reset)", () => {
|
|
75
|
+
assert.deepEqual(stripBackgroundSgrParams([49]), []);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("stripBackgroundSgrParams keeps param 49 not confused with rgb component", () => {
|
|
79
|
+
// The function only sees the parsed numeric params, so 49 in an RGB component
|
|
80
|
+
// is already parsed as [48, 2, 12, 49, 200]. stripBackgroundSgrParams
|
|
81
|
+
// should skip the entire 48;2;r;g;b sequence, not treat the color value 49 as reset.
|
|
82
|
+
const result = stripBackgroundSgrParams([48, 2, 12, 49, 200]);
|
|
83
|
+
assert.deepEqual(result, []);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("stripBackgroundSgrParams replaces reset param 0 with style reset params", () => {
|
|
87
|
+
const result = stripBackgroundSgrParams([0]);
|
|
88
|
+
assert.deepEqual(result, [...STYLE_RESET_PARAMS]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("stripBackgroundSgrParams strips 8-bit background (48;5;N)", () => {
|
|
92
|
+
assert.deepEqual(stripBackgroundSgrParams([48, 5, 196]), []);
|
|
93
|
+
assert.deepEqual(stripBackgroundSgrParams([48, 5, 255]), []);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("stripBackgroundSgrParams strips RGB background (48;2;r;g;b)", () => {
|
|
97
|
+
assert.deepEqual(stripBackgroundSgrParams([48, 2, 10, 20, 30]), []);
|
|
98
|
+
assert.deepEqual(stripBackgroundSgrParams([48, 2, 255, 128, 0]), []);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("stripBackgroundSgrParams keeps foreground 8-bit color (38;5;N)", () => {
|
|
102
|
+
assert.deepEqual(stripBackgroundSgrParams([38, 5, 196]), [38, 5, 196]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("stripBackgroundSgrParams keeps foreground RGB color (38;2;r;g;b)", () => {
|
|
106
|
+
assert.deepEqual(stripBackgroundSgrParams([38, 2, 12, 49, 200]), [38, 2, 12, 49, 200]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("stripBackgroundSgrParams strips direct background codes 40-47", () => {
|
|
110
|
+
for (let code = 40; code <= 47; code++) {
|
|
111
|
+
assert.deepEqual(stripBackgroundSgrParams([code]), [], `param ${code} should be stripped`);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("stripBackgroundSgrParams strips bright background codes 100-107", () => {
|
|
116
|
+
for (let code = 100; code <= 107; code++) {
|
|
117
|
+
assert.deepEqual(stripBackgroundSgrParams([code]), [], `param ${code} should be stripped`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("stripBackgroundSgrParams keeps non-background params", () => {
|
|
122
|
+
assert.deepEqual(stripBackgroundSgrParams([1]), [1]); // bold
|
|
123
|
+
assert.deepEqual(stripBackgroundSgrParams([3]), [3]); // italic
|
|
124
|
+
assert.deepEqual(stripBackgroundSgrParams([4]), [4]); // underline
|
|
125
|
+
assert.deepEqual(stripBackgroundSgrParams([7]), [7]); // inverse
|
|
126
|
+
assert.deepEqual(stripBackgroundSgrParams([9]), [9]); // strikethrough
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("stripBackgroundSgrParams handles incomplete 48;5 sequence (missing color value)", () => {
|
|
130
|
+
// Only 48 and 5 present, no color value → skips index+2 (48 and 5)
|
|
131
|
+
assert.deepEqual(stripBackgroundSgrParams([48, 5]), []);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("stripBackgroundSgrParams handles incomplete 48;2 sequence (missing color components)", () => {
|
|
135
|
+
// Only 48 and 2 and partial RGB → skips index+4 (48,2,r,g,b)
|
|
136
|
+
assert.deepEqual(stripBackgroundSgrParams([48, 2, 100]), []);
|
|
137
|
+
assert.deepEqual(stripBackgroundSgrParams([48, 2, 100, 200]), []);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("stripBackgroundSgrParams handles mixed params preserving non-background and stripping background", () => {
|
|
141
|
+
const result = stripBackgroundSgrParams([1, 49, 3, 40, 4, 48, 5, 196, 7]);
|
|
142
|
+
// 1 kept, 49 stripped, 3 kept, 40 stripped, 4 kept, 48;5;196 stripped, 7 kept
|
|
143
|
+
assert.deepEqual(result, [1, 3, 4, 7]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("stripBackgroundSgrParams handles full reset zero before background params", () => {
|
|
147
|
+
// [0, 49] → param 0 replaced with style reset params, param 49 stripped
|
|
148
|
+
const result = stripBackgroundSgrParams([0, 49]);
|
|
149
|
+
assert.deepEqual(result, [...STYLE_RESET_PARAMS]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ─── sanitizeAnsiForThemedOutput ───────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
test("sanitizeAnsiForThemedOutput returns empty string unchanged", () => {
|
|
155
|
+
assert.equal(sanitizeAnsiForThemedOutput(""), "");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("sanitizeAnsiForThemedOutput returns string with no ANSI codes unchanged", () => {
|
|
159
|
+
const text = "hello world";
|
|
160
|
+
assert.equal(sanitizeAnsiForThemedOutput(text), text);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("sanitizeAnsiForThemedOutput strips background reset from SGR sequence", () => {
|
|
164
|
+
const input = "\x1b[1;49;32mhello";
|
|
165
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
166
|
+
// 1 (bold) kept, 49 (bg reset) stripped, 32 (fg green) kept
|
|
167
|
+
assert.equal(result, "\x1b[1;32mhello");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("sanitizeAnsiForThemedOutput keeps foreground RGB with 49 component value", () => {
|
|
171
|
+
// \x1b[38;2;12;49;200m → RGB fg with 49 as GREEN component, should be kept
|
|
172
|
+
const input = "\x1b[38;2;12;49;200mhello";
|
|
173
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
174
|
+
// stripBackgroundSgrParams keeps the whole RGB foreground sequence
|
|
175
|
+
assert.equal(result, input);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("sanitizeAnsiForThemedOutput keeps foreground 8-bit colors", () => {
|
|
179
|
+
const input = "\x1b[38;5;82mhello";
|
|
180
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
181
|
+
assert.equal(result, input);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("sanitizeAnsiForThemedOutput strips background 8-bit colors", () => {
|
|
185
|
+
const input = "\x1b[48;5;196mhello";
|
|
186
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
187
|
+
assert.equal(result, "hello");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("sanitizeAnsiForThemedOutput strips RGB background colors while keeping foreground", () => {
|
|
191
|
+
const input = "\x1b[1;38;2;10;20;30;48;2;100;200;50mhello";
|
|
192
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
193
|
+
// Should keep 1 (bold) and RGB foreground 38;2;10;20;30
|
|
194
|
+
assert.equal(result, "\x1b[1;38;2;10;20;30mhello");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("sanitizeAnsiForThemedOutput preserves OSC sequences (not matched by SGR pattern)", () => {
|
|
198
|
+
const input = "\x1b]0;my title\x07hello";
|
|
199
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
200
|
+
assert.equal(result, input);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("sanitizeAnsiForThemedOutput preserves CSI cursor sequences", () => {
|
|
204
|
+
const input = "\x1b[A\x1b[5Chello";
|
|
205
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
206
|
+
// \x1b[A and \x1b[5C are not SGR (don't end with 'm'), pass through
|
|
207
|
+
assert.equal(result, input);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("sanitizeAnsiForThemedOutput preserves bell characters", () => {
|
|
211
|
+
const input = "hello\x07world";
|
|
212
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
213
|
+
assert.equal(result, input);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("sanitizeAnsiForThemedOutput handles string with only non-SGR escape sequences", () => {
|
|
217
|
+
const input = "\x1b[2J\x1b[Hclear";
|
|
218
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
219
|
+
// Not SGR (don't end with m), pass through
|
|
220
|
+
assert.equal(result, input);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("sanitizeAnsiForThemedOutput strips empty SGR sequences", () => {
|
|
224
|
+
// \x1b[m → toSgrParams("") → [0] → stripBackgroundSgrParams([0]) → STYLE_RESET_PARAMS → still non-empty
|
|
225
|
+
// So it doesn't strip reset entirely, it replaces with style reset params
|
|
226
|
+
const input = "\x1b[mhello";
|
|
227
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
228
|
+
assert.equal(result, "\x1b[39;22;23;24;25;27;28;29;59mhello");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("sanitizeAnsiForThemedOutput strips SGR that becomes empty after background removal", () => {
|
|
232
|
+
// \x1b[49m → toSgrParams("49") → [49] → stripBackgroundSgrParams([49]) → [] → returns ""
|
|
233
|
+
const input = "\x1b[49mhello";
|
|
234
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
235
|
+
assert.equal(result, "hello");
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("sanitizeAnsiForThemedOutput handles multiple SGR sequences in one string", () => {
|
|
239
|
+
const input = "\x1b[1m\x1b[49mbold\x1b[49m\x1b[22m";
|
|
240
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
241
|
+
// [1] → [1] kept; [49] → [] stripped; [49] → [] stripped; [22] → [22] kept
|
|
242
|
+
assert.equal(result, "\x1b[1mbold\x1b[22m");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("sanitizeAnsiForThemedOutput handles text without any escape sequences", () => {
|
|
246
|
+
assert.equal(sanitizeAnsiForThemedOutput("plain text"), "plain text");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("sanitizeAnsiForThemedOutput handles non-SGR CSI sequences mixed with SGR", () => {
|
|
250
|
+
const input = "\x1b[1m\x1b[49m\x1b[5Cmixed";
|
|
251
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
252
|
+
// \x1b[5C is not SGR (doesn't end with m), passes through
|
|
253
|
+
assert.equal(result, "\x1b[1m\x1b[5Cmixed");
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("sanitizeAnsiForThemedOutput handles multiple semicolons producing empty params", () => {
|
|
257
|
+
// \x1b[1;;32m → toSgrParams("1;;32") → [1, 32] (NaN filtered) → stripBackgroundSgrParams → [1, 32]
|
|
258
|
+
const input = "\x1b[1;;32mhello";
|
|
259
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
260
|
+
assert.equal(result, "\x1b[1;32mhello");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// ─── sequenceResetsBackground behavior exported through sanitizeAnsiForThemedOutput ──
|
|
264
|
+
// These tests exercise the behavior of sequenceResetsBackground (private in diff-renderer)
|
|
265
|
+
// by verifying that sanitizeAnsiForThemedOutput does NOT incorrectly strip RGB foreground
|
|
266
|
+
// sequences containing the value 49 as a component.
|
|
267
|
+
|
|
268
|
+
test("sanitizeAnsiForThemedOutput preserves RGB foreground with 49 in green component (issue #8/#3 regression guard)", () => {
|
|
269
|
+
// \x1b[38;2;12;49;200m → toSgrParams → [38, 2, 12, 49, 200]
|
|
270
|
+
// stripBackgroundSgrParams([38, 2, 12, 49, 200]):
|
|
271
|
+
// index=0: param=38, not 0, not 49, not 40-47, not 100-107, not 48 → push 38
|
|
272
|
+
// index=1: param=2 → push 2
|
|
273
|
+
// index=2: param=12 → push 12
|
|
274
|
+
// index=3: param=49 → push 49 (only stripped when param === 49 directly, not inside RGB)
|
|
275
|
+
// index=4: param=200 → push 200
|
|
276
|
+
// Result: [38, 2, 12, 49, 200] → preserved
|
|
277
|
+
const input = "\x1b[38;2;12;49;200mtest";
|
|
278
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
279
|
+
assert.equal(result, input);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("sanitizeAnsiForThemedOutput preserves 8-bit foreground even when color value is 49", () => {
|
|
283
|
+
// \x1b[38;5;49m → color 49 is a valid 8-bit color (not background reset)
|
|
284
|
+
const input = "\x1b[38;5;49mtest";
|
|
285
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
286
|
+
assert.equal(result, input);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("sanitizeAnsiForThemedOutput correctly strips actual background reset (49) in mixed seq", () => {
|
|
290
|
+
// \x1b[1;49;32m → [1, 49, 32] → 1 kept, 49 stripped, 32 kept → \x1b[1;32m
|
|
291
|
+
const input = "\x1b[1;49;32mtest";
|
|
292
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
293
|
+
assert.equal(result, "\x1b[1;32mtest");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ─── ANSI_SGR_PATTERN edge cases ───────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
test("ANSI_SGR_PATTERN matches standard SGR sequences", () => {
|
|
299
|
+
const matches = "\x1b[31m\x1b[1;32m\x1b[0m".match(ANSI_SGR_PATTERN);
|
|
300
|
+
assert.deepEqual(matches, ["\x1b[31m", "\x1b[1;32m", "\x1b[0m"]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("ANSI_SGR_PATTERN does not match CSI sequences not ending in m", () => {
|
|
304
|
+
const matches = "\x1b[A\x1b[5C\x1b[2J".match(ANSI_SGR_PATTERN);
|
|
305
|
+
assert.equal(matches, null);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("ANSI_SGR_PATTERN does not match OSC sequences", () => {
|
|
309
|
+
const matches = "\x1b]0;title\x07".match(ANSI_SGR_PATTERN);
|
|
310
|
+
assert.equal(matches, null);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ─── colon-form SGR (if supported) ──────────────────────────────────────────
|
|
314
|
+
// Current implementation uses semicolons only. Colon-form params like
|
|
315
|
+
// \x1b[38:2:12:49:200m are NOT currently handled by toSgrParams which splits on ";"
|
|
316
|
+
// Test that they behave as undefined/non-matching.
|
|
317
|
+
|
|
318
|
+
test("toSgrParams partial-parse on colon-form (colon-form SGR not supported)", () => {
|
|
319
|
+
// "38:2:12:49:200" → split(";") → ["38:2:12:49:200"]
|
|
320
|
+
// parseInt("38:2:12:49:200", 10) → 38 (parseInt stops at the first non-digit)
|
|
321
|
+
const result = toSgrParams("38:2:12:49:200");
|
|
322
|
+
assert.deepEqual(result, [38]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("sanitizeAnsiForThemedOutput handles colon-form SGR sequence as non-SGR passthrough", () => {
|
|
326
|
+
// \x1b[38:2:12:49:200m is an SGR sequence but uses colons.
|
|
327
|
+
// The regex \x1b\[([0-9;]*)m will match "38:2:12:49:200" because colons are not [0-9;]
|
|
328
|
+
// Wait: [0-9;] matches digits and semicolons only, NOT colons.
|
|
329
|
+
// So \x1b[38:2:12:49:200m would NOT match ANSI_SGR_PATTERN since ":" is not in [0-9;].
|
|
330
|
+
// Therefore the entire sequence passes through unchanged.
|
|
331
|
+
const input = "\x1b[38:2:12:49:200mhello";
|
|
332
|
+
const result = sanitizeAnsiForThemedOutput(input);
|
|
333
|
+
assert.equal(result, input);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
// ─── edge case: non-string at runtime ──────────────────────────────────────
|
|
337
|
+
|
|
338
|
+
test("sanitizeAnsiForThemedOutput handles null/undefined gracefully at runtime", () => {
|
|
339
|
+
assert.equal(sanitizeAnsiForThemedOutput(null as unknown as string), null);
|
|
340
|
+
assert.equal(sanitizeAnsiForThemedOutput(undefined as unknown as string), undefined);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("sanitizeAnsiForThemedOutput throws for non-string input at runtime", () => {
|
|
344
|
+
assert.throws(() => sanitizeAnsiForThemedOutput(42 as unknown as string), /includes/);
|
|
345
|
+
});
|