santree 0.5.2 → 0.5.3
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.
|
@@ -42,17 +42,72 @@ const CLAUDE_VERSION = getInstalledClaudeVersion() ?? "";
|
|
|
42
42
|
* R100\told/path\tnew/path
|
|
43
43
|
* For renames/copies, the status code has a similarity suffix we strip.
|
|
44
44
|
*/
|
|
45
|
+
/**
|
|
46
|
+
* Split combined-parameter SGR sequences (e.g. `\x1b[48;2;R;G;B;38;2;R;G;B m`)
|
|
47
|
+
* into separate single-attribute SGRs (`\x1b[48;2;...m\x1b[38;2;...m`).
|
|
48
|
+
*
|
|
49
|
+
* Why: Ink uses `slice-ansi` to clip text horizontally, and `slice-ansi`
|
|
50
|
+
* miscounts visible width on combined RGB bg+fg SGRs — it cuts the line at
|
|
51
|
+
* roughly half the requested visible width. Delta emits exactly this combined
|
|
52
|
+
* form on every styled token, so the diff pane was rendering content cut at
|
|
53
|
+
* arbitrary points (e.g. `from datetime i` instead of `from datetime import
|
|
54
|
+
* timedelta`). Splitting them sidesteps the slice-ansi bug without losing any
|
|
55
|
+
* styling — the terminal renders the two SGRs identically to the combined one.
|
|
56
|
+
*/
|
|
57
|
+
function splitCombinedSgr(s) {
|
|
58
|
+
return s.replace(/\x1b\[([0-9;]+)m/g, (_match, params) => {
|
|
59
|
+
const tokens = params.split(";");
|
|
60
|
+
const groups = [];
|
|
61
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
62
|
+
const t = tokens[i];
|
|
63
|
+
if ((t === "38" || t === "48") && tokens[i + 1] === "2") {
|
|
64
|
+
groups.push([t, "2", tokens[i + 2], tokens[i + 3], tokens[i + 4]].join(";"));
|
|
65
|
+
i += 4;
|
|
66
|
+
}
|
|
67
|
+
else if ((t === "38" || t === "48") && tokens[i + 1] === "5") {
|
|
68
|
+
groups.push([t, "5", tokens[i + 2]].join(";"));
|
|
69
|
+
i += 2;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
groups.push(t);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (groups.length <= 1)
|
|
76
|
+
return `\x1b[${params}m`;
|
|
77
|
+
return groups.map((g) => `\x1b[${g}m`).join("");
|
|
78
|
+
});
|
|
79
|
+
}
|
|
45
80
|
/**
|
|
46
81
|
* Pipe `git diff` output through an external tool (e.g. delta) and return the
|
|
47
82
|
* combined ANSI output. Uses spawn pipes — no shell — so the tool name is safe
|
|
48
83
|
* even though we already validate it in getDiffTool().
|
|
49
84
|
*/
|
|
50
|
-
function runPipedDiff(cwd,
|
|
85
|
+
function runPipedDiff(cwd, gitArgs, tool, themeMode) {
|
|
51
86
|
return new Promise((resolve, reject) => {
|
|
52
|
-
const git = spawn("git", ["-C", cwd,
|
|
87
|
+
const git = spawn("git", ["-C", cwd, ...gitArgs], {
|
|
53
88
|
stdio: ["ignore", "pipe", "pipe"],
|
|
54
89
|
});
|
|
55
|
-
|
|
90
|
+
// Delta's syntax theme defaults are tuned for dark backgrounds — pale
|
|
91
|
+
// Monokai foreground on a light terminal becomes invisible. Force the
|
|
92
|
+
// theme flag matching santree's detected mode so colors stay readable.
|
|
93
|
+
const pagerArgs = tool === "delta" ? [themeMode === "light" ? "--light" : "--dark"] : [];
|
|
94
|
+
// Disable hyperlinks for delta: OSC 8 sequences (`\x1b]8;...`) are not
|
|
95
|
+
// handled by truncateVisible() — its CSI-only regex counts the URL
|
|
96
|
+
// bytes as visible characters, mangling line truncation and breaking
|
|
97
|
+
// terminal rendering of the wrapped text. Delta's CLI rejects an
|
|
98
|
+
// inline `--hyperlinks=false`, so override via GIT_CONFIG_PARAMETERS
|
|
99
|
+
// (delta reads its config from git). Also drop line-numbers — they
|
|
100
|
+
// eat ~6 cols of an already-narrow right pane.
|
|
101
|
+
const pagerEnv = tool === "delta"
|
|
102
|
+
? {
|
|
103
|
+
...process.env,
|
|
104
|
+
GIT_CONFIG_PARAMETERS: "'delta.hyperlinks=false' 'delta.line-numbers=false'",
|
|
105
|
+
}
|
|
106
|
+
: process.env;
|
|
107
|
+
const pager = spawn(tool, pagerArgs, {
|
|
108
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
109
|
+
env: pagerEnv,
|
|
110
|
+
});
|
|
56
111
|
let out = "";
|
|
57
112
|
let err = "";
|
|
58
113
|
git.stdout.pipe(pager.stdin);
|
|
@@ -72,7 +127,7 @@ function runPipedDiff(cwd, mergeBase, filePath, tool) {
|
|
|
72
127
|
reject(new Error(err || `${tool} exited with code ${code}`));
|
|
73
128
|
}
|
|
74
129
|
else {
|
|
75
|
-
resolve(out);
|
|
130
|
+
resolve(splitCombinedSgr(out));
|
|
76
131
|
}
|
|
77
132
|
});
|
|
78
133
|
});
|
|
@@ -664,16 +719,24 @@ export default function Dashboard() {
|
|
|
664
719
|
if (file.isUntracked) {
|
|
665
720
|
// Untracked files aren't in `git diff` output — fake a "full
|
|
666
721
|
// addition" diff via --no-index against /dev/null. git exits 1
|
|
667
|
-
// when files differ; that's expected, so capture stdout
|
|
668
|
-
//
|
|
669
|
-
|
|
670
|
-
|
|
722
|
+
// when files differ; that's expected, so we capture stdout
|
|
723
|
+
// regardless. Pipe through the configured tool when set so
|
|
724
|
+
// untracked files get the same syntax highlighting as tracked
|
|
725
|
+
// ones; otherwise fall back to spawnAsync + manual colorize.
|
|
726
|
+
if (tool) {
|
|
727
|
+
const content = await runPipedDiff(cwd, ["diff", "--color=always", "--no-index", "--", "/dev/null", file.path], tool, theme.mode);
|
|
728
|
+
dispatch({ type: "DIFF_CONTENT_LOADED", content });
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
const { output } = await spawnAsync("git", ["-C", cwd, "diff", "--no-color", "--no-index", "--", "/dev/null", file.path], { cwd });
|
|
732
|
+
dispatch({ type: "DIFF_CONTENT_LOADED", content: output });
|
|
733
|
+
}
|
|
671
734
|
}
|
|
672
735
|
else if (tool) {
|
|
673
736
|
// Pipe git diff (with colors enabled so the tool can pass them
|
|
674
737
|
// through if desired) into the configured tool. Use spawn pipes
|
|
675
738
|
// rather than shell to avoid quoting concerns.
|
|
676
|
-
const content = await runPipedDiff(cwd, mergeBase, file.path, tool);
|
|
739
|
+
const content = await runPipedDiff(cwd, ["diff", "--color=always", mergeBase, "--", file.path], tool, theme.mode);
|
|
677
740
|
dispatch({ type: "DIFF_CONTENT_LOADED", content });
|
|
678
741
|
}
|
|
679
742
|
else {
|
|
@@ -693,6 +756,7 @@ export default function Dashboard() {
|
|
|
693
756
|
state.diffMergeBase,
|
|
694
757
|
state.diffFileIndex,
|
|
695
758
|
state.diffFiles,
|
|
759
|
+
theme.mode,
|
|
696
760
|
]);
|
|
697
761
|
// ── Actions ───────────────────────────────────────────────────────
|
|
698
762
|
const launchWorkInTmux = useCallback(async (di, mode, worktreePath, contextFile) => {
|
|
@@ -294,6 +294,13 @@ export default function DiffOverlay({ width, height, ticketId, baseBranch, files
|
|
|
294
294
|
// rightWidth includes the paddingLeft={1} of the wrapper Box,
|
|
295
295
|
// so usable column count is rightWidth - 1.
|
|
296
296
|
const cell = truncateVisible(line.text || " ", Math.max(1, rightWidth - 1));
|
|
297
|
-
|
|
297
|
+
// wrap="truncate" prevents Ink from soft-wrapping. Default
|
|
298
|
+
// `wrap` mode measures byte length (counting ANSI escape
|
|
299
|
+
// bytes as visible chars), which makes color-heavy lines
|
|
300
|
+
// like syntax-highlighted code wrap *very* early — visible
|
|
301
|
+
// content gets clobbered by the next row. truncateVisible
|
|
302
|
+
// has already sized the cell, so `truncate` is a no-op for
|
|
303
|
+
// already-fitting lines.
|
|
304
|
+
return (_jsx(Text, { color: line.color, bold: line.bold, dimColor: line.dim, wrap: "truncate", children: cell }, i));
|
|
298
305
|
})) })] }))] }));
|
|
299
306
|
}
|