Skip to content

Commit 334cec0

Browse files
committed
Ignore prose apostrophes and quotes when finding image paths
Contractions were pairing with later path quotes, and prose-quoted sentences were resolving as bogus relative paths. Only treat path- shaped wrappers as wrappers, and still find absolute paths inside ordinary quotes.
1 parent ac85e36 commit 334cec0

3 files changed

Lines changed: 78 additions & 2 deletions

File tree

src/tui/image-attachments.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,22 @@ describe("findImagePathMentions", () => {
175175
{ raw: "/repo/second.JPG", path: "/repo/second.JPG" },
176176
]);
177177
});
178+
179+
test("does not let contractions steal single-quoted path wrappers", () => {
180+
const observed = "/tmp/Screenshot 2026-09-10 at 11.42.07\u202fAM.png";
181+
expect(findImagePathMentions(`what's in '${observed}'?`, "/repo")).toEqual([
182+
{ raw: `'${observed}'`, path: observed },
183+
]);
184+
expect(
185+
findImagePathMentions(`don't use '/tmp/shot.png' please`, "/repo"),
186+
).toEqual([{ raw: "'/tmp/shot.png'", path: "/tmp/shot.png" }]);
187+
});
188+
189+
test("does not let prose quotes invent a relative path over an absolute mention", () => {
190+
expect(
191+
findImagePathMentions(`He said "look at /tmp/shot.png" today`, "/repo"),
192+
).toEqual([{ raw: "/tmp/shot.png", path: "/tmp/shot.png" }]);
193+
});
178194
});
179195

180196
describe("image attachment helpers", () => {

src/tui/image-attachments.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,23 @@ function scanImagePathLine(
104104
const ch = text[i];
105105
if (ch === undefined) break;
106106
if (WRAPPERS.has(ch)) {
107+
// Contractions/possessives (`what's`, `don't`) are not quote openers.
108+
if (ch === "'" && i > start && isWordChar(text[i - 1])) {
109+
i += 1;
110+
continue;
111+
}
107112
const close = text.indexOf(ch, i + 1);
108113
if (close !== -1 && close < end) {
109-
const raw = text.slice(i, close + 1);
110114
const inner = text.slice(i + 1, close);
111-
push(raw, normalizeImagePathCandidate(inner, cwd, true));
115+
if (looksLikeQuotedImagePath(inner)) {
116+
const raw = text.slice(i, close + 1);
117+
push(raw, normalizeImagePathCandidate(inner, cwd, true));
118+
i = close + 1;
119+
continue;
120+
}
121+
// Balanced quotes around prose are not path wrappers. Search inside
122+
// so an absolute path can still be found, then resume after the closer.
123+
scanImagePathLine(text, i + 1, close, cwd, push);
112124
i = close + 1;
113125
continue;
114126
}
@@ -130,6 +142,40 @@ function scanImagePathLine(
130142
}
131143
}
132144

145+
function isWordChar(ch: string | undefined): boolean {
146+
if (ch === undefined || ch.length !== 1) return false;
147+
return (
148+
(ch >= "0" && ch <= "9") ||
149+
(ch >= "A" && ch <= "Z") ||
150+
(ch >= "a" && ch <= "z")
151+
);
152+
}
153+
154+
function looksLikeQuotedImagePath(inner: string): boolean {
155+
if (inner.startsWith("file://")) return true;
156+
if (inner === "~" || inner.startsWith("~/") || inner.startsWith("~\\"))
157+
return true;
158+
if (inner.startsWith("/") || inner.startsWith("\\")) return true;
159+
if (
160+
inner.startsWith("./") ||
161+
inner.startsWith("../") ||
162+
inner.startsWith(".\\") ||
163+
inner.startsWith("..\\")
164+
) {
165+
return true;
166+
}
167+
const drive = inner[0];
168+
const sep = inner[2];
169+
return (
170+
inner.length >= 3 &&
171+
drive !== undefined &&
172+
sep !== undefined &&
173+
((drive >= "A" && drive <= "Z") || (drive >= "a" && drive <= "z")) &&
174+
inner[1] === ":" &&
175+
(sep === "/" || sep === "\\")
176+
);
177+
}
178+
133179
function canStartUnquotedPath(text: string, i: number, end: number): boolean {
134180
if (i >= end) return false;
135181
if (text.startsWith("file://", i)) return true;

src/tui/prompt-attachments.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ describe("ingestPathMentions", () => {
4343
expect(result.attachments).toHaveLength(1);
4444
});
4545

46+
test("replaces the full wrapped token including quotes", async () => {
47+
const load = async (path: string): Promise<AttachImageResult> => ({
48+
ok: true,
49+
attachment: { ...attachment("shot.png"), path },
50+
});
51+
const result = await ingestPathMentions(
52+
"look at '/tmp/shot.png' please",
53+
"/repo",
54+
load,
55+
);
56+
expect(result.text).toBe("look at [Attached image: shot.png] please");
57+
expect(result.attachments).toHaveLength(1);
58+
});
59+
4660
test("keeps the raw path when loading fails", async () => {
4761
const load = async (): Promise<AttachImageResult> => ({
4862
ok: false,

0 commit comments

Comments
 (0)