Skip to content

Commit 981156c

Browse files
committed
Only match code-consuming trigger words in command position
segmentWords split on raw whitespace with no quote-awareness, so a trigger word inside a quoted payload — a commit message mentioning 'source', a heredoc line mentioning 'env' — falsely marked the segment as code-consuming and suppressed collapsing it. Rewrite segmentWords to walk the segment and skip quoted and heredoc-body spans, so only the actual command and its flags are considered.
1 parent 596cbb3 commit 981156c

2 files changed

Lines changed: 88 additions & 4 deletions

File tree

src/tui/command-display.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,3 +210,22 @@ test("collapseSegmentPayloads never collapses a nohup-wrapped bash -c invocation
210210
expect(collapseSegmentPayloads(segment)).toEqual({ display: segment, payloads: [] });
211211
});
212212

213+
test("collapseSegmentPayloads still collapses a commit message containing a trigger word in quoted text", () => {
214+
const segment = 'git commit -m "please source of truth\nfor this change"';
215+
const { display, payloads } = collapseSegmentPayloads(segment);
216+
expect(display).toBe("git commit -m <message, 2 lines>");
217+
expect(payloads).toEqual([{ placeholder: "<message, 2 lines>", lines: ["please source of truth", "for this change"] }]);
218+
});
219+
220+
test("collapseSegmentPayloads still collapses a quoted argument mentioning env in its text", () => {
221+
const segment = 'echo "the env for this feature\nis staging"';
222+
const { display } = collapseSegmentPayloads(segment);
223+
expect(display).toBe("echo <text, 2 lines>");
224+
});
225+
226+
test("collapseSegmentPayloads still collapses a normal long commit-message heredoc", () => {
227+
const segment = "git commit -F <<'EOF'\nsummary line\nmore detail\nEOF\n";
228+
const { display, payloads } = collapseSegmentPayloads(segment);
229+
expect(display).toBe("git commit -F <<'EOF' <heredoc, 2 lines>");
230+
expect(payloads).toEqual([{ placeholder: "<heredoc, 2 lines>", lines: ["summary line", "more detail"] }]);
231+
});

src/tui/command-display.ts

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,11 +285,76 @@ const INTERPRETER_CODE_FLAGS: Record<string, readonly string[]> = {
285285
php: ["-r"],
286286
};
287287

288-
// Crude whitespace tokenizing is enough here: quoting doesn't change whether
289-
// an interpreter name or a `-c` flag literally appears as a word, and this is
290-
// display-only guesswork (see the file header) — never used for classification.
288+
// Command-position words only: the program name and its flags, never text
289+
// inside a quoted argument or heredoc body. A naive whitespace split would
290+
// let a trigger word incidentally appearing inside a quoted payload (a commit
291+
// message mentioning "source", a heredoc line mentioning "env") falsely mark
292+
// the segment as code-consuming and suppress collapsing it — this walk skips
293+
// quoted/heredoc spans entirely so only the actual command and its arguments
294+
// are considered. Display-only guesswork (see the file header) — never used
295+
// for classification.
291296
function segmentWords(segment: string): string[] {
292-
return segment.split(/\s+/).filter((word) => word.length > 0);
297+
const words: string[] = [];
298+
let current = "";
299+
let quote: '"' | "'" | "`" | null = null;
300+
let heredocMarker: string | null = null;
301+
302+
const push = (): void => {
303+
if (current.length > 0) words.push(current);
304+
current = "";
305+
};
306+
307+
let i = 0;
308+
while (i < segment.length) {
309+
const ch = segment[i] as string;
310+
311+
if (heredocMarker !== null) {
312+
if (ch === "\n") {
313+
let lineEnd = segment.indexOf("\n", i + 1);
314+
if (lineEnd === -1) lineEnd = segment.length;
315+
if (segment.slice(i + 1, lineEnd).trim() === heredocMarker) {
316+
heredocMarker = null;
317+
i = lineEnd;
318+
}
319+
}
320+
i++;
321+
continue;
322+
}
323+
324+
if (quote !== null) {
325+
if (ch === quote) quote = null;
326+
i++;
327+
continue;
328+
}
329+
330+
if (ch === '"' || ch === "'" || ch === "`") {
331+
push();
332+
quote = ch;
333+
i++;
334+
continue;
335+
}
336+
337+
if (ch === "<" && segment[i + 1] === "<") {
338+
const marker = parseHeredocMarker(segment, i);
339+
if (marker !== null) {
340+
push();
341+
heredocMarker = marker;
342+
while (i < segment.length && segment[i] !== "\n") i++;
343+
continue;
344+
}
345+
}
346+
347+
if (ch === " " || ch === "\t" || ch === "\n") {
348+
push();
349+
i++;
350+
continue;
351+
}
352+
353+
current += ch;
354+
i++;
355+
}
356+
push();
357+
return words;
293358
}
294359

295360
// The POSIX basename of a word naming a program: strips any directory

0 commit comments

Comments
 (0)