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, mergeBase, filePath, tool) {
85
+ function runPipedDiff(cwd, gitArgs, tool, themeMode) {
51
86
  return new Promise((resolve, reject) => {
52
- const git = spawn("git", ["-C", cwd, "diff", "--color=always", mergeBase, "--", filePath], {
87
+ const git = spawn("git", ["-C", cwd, ...gitArgs], {
53
88
  stdio: ["ignore", "pipe", "pipe"],
54
89
  });
55
- const pager = spawn(tool, [], { stdio: ["pipe", "pipe", "pipe"] });
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 via
668
- // spawnAsync rather than execAsync (which throws on non-zero).
669
- const { output } = await spawnAsync("git", ["-C", cwd, "diff", "--no-color", "--no-index", "--", "/dev/null", file.path], { cwd });
670
- dispatch({ type: "DIFF_CONTENT_LOADED", content: output });
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
- return (_jsx(Text, { color: line.color, bold: line.bold, dimColor: line.dim, children: cell }, i));
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "santree",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Git worktree manager",
5
5
  "license": "MIT",
6
6
  "author": "Santiago Toscanini",