diff --git a/packages/kilo-vscode/tests/unit/terminal-architecture.test.ts b/packages/kilo-vscode/tests/unit/terminal-architecture.test.ts index a8ddfb478e91..36f8da8cc122 100644 --- a/packages/kilo-vscode/tests/unit/terminal-architecture.test.ts +++ b/packages/kilo-vscode/tests/unit/terminal-architecture.test.ts @@ -19,7 +19,7 @@ describe("terminal context architecture", () => { it("keeps webview terminal attachment logic outside PromptInput", () => { const prompt = src("webview-ui/src/components/chat/PromptInput.tsx") const hook = src("webview-ui/src/hooks/useTerminalContext.ts") - const util = src("webview-ui/src/hooks/terminal-context-utils.ts") + const util = src("webview-ui/src/hooks/context-mention-utils.ts") expect(prompt).toContain("useTerminalContext") expect(prompt).toContain("resolveAttachment(message, id, readTerminalContext(props.terminalContext))") diff --git a/packages/kilo-vscode/tests/unit/terminal-context-utils.test.ts b/packages/kilo-vscode/tests/unit/terminal-context-utils.test.ts index 1b40840980de..a0f7cb6fae7b 100644 --- a/packages/kilo-vscode/tests/unit/terminal-context-utils.test.ts +++ b/packages/kilo-vscode/tests/unit/terminal-context-utils.test.ts @@ -1,26 +1,30 @@ import { describe, expect, it } from "bun:test" -import { - buildTerminalAttachment, - findTerminalMention, - hasTerminalMention, -} from "../../webview-ui/src/hooks/terminal-context-utils" +import { buildGitChangesAttachment as git } from "../../webview-ui/src/hooks/git-changes-context-utils" +import { buildTerminalAttachment as terminal } from "../../webview-ui/src/hooks/terminal-context-utils" -describe("terminal context utils", () => { - it("detects standalone terminal mentions", () => { - expect(hasTerminalMention("see @terminal output")).toBe(true) - expect(hasTerminalMention("see foo@terminal output")).toBe(false) - expect(hasTerminalMention("see @terminal-output")).toBe(false) - }) +describe.each([ + ["terminal", terminal, "terminal-output.txt"], + ["git-changes", git, "git-changes.txt"], +])("%s context utils", (token, build, filename) => { + const value = `@${token}` - it("returns mention source range", () => { - expect(findTerminalMention("hello @terminal")!).toEqual({ value: "@terminal", start: 6, end: 15 }) + it("rejects missing mentions and false prefixes or suffixes", () => { + for (const text of ["plain text", `foo${value}`, `${value}-output`, `${value}.`]) { + expect(build(text, "content")).toBeUndefined() + } }) - it("builds a text attachment with source metadata", () => { - const attachment = buildTerminalAttachment("check @terminal", "npm failed")! - expect(attachment.mime).toBe("text/plain") - expect(attachment.filename).toBe("terminal-output.txt") - expect(attachment.url).toBe("data:text/plain;charset=utf-8,npm%20failed") - expect(attachment.source?.text).toEqual({ value: "@terminal", start: 6, end: 15 }) + it("preserves whitespace boundaries, first spans, repeated calls and encoded content", () => { + for (const prefix of ["", "hello ", "\r\n\t", "\u00a0"]) { + const mention = { value, start: prefix.length, end: prefix.length + value.length } + const expected = { + mime: "text/plain", + filename, + url: "data:text/plain;charset=utf-8,%C3%A9%20%25%26%23%3F%0D%0A", + source: { type: "file", path: filename, text: mention }, + } + expect(build(`${prefix}${value} ${value}`, "é %&#?\r\n")).toEqual(expected) + expect(build(`${prefix}${value}`, "é %&#?\r\n")).toEqual(expected) + } }) }) diff --git a/packages/kilo-vscode/webview-ui/src/hooks/context-mention-utils.ts b/packages/kilo-vscode/webview-ui/src/hooks/context-mention-utils.ts new file mode 100644 index 000000000000..886e17b49eeb --- /dev/null +++ b/packages/kilo-vscode/webview-ui/src/hooks/context-mention-utils.ts @@ -0,0 +1,37 @@ +import type { FileAttachment } from "../types/messages" + +export type Mention = { + value: string + start: number + end: number +} + +export function find(text: string, pattern: RegExp, token: string): Mention | undefined { + pattern.lastIndex = 0 + const match = pattern.exec(text) + if (!match) return undefined + + const prefix = match[1] ?? "" + const start = match.index + prefix.length + const value = `@${token}` + return { value, start, end: start + value.length } +} + +export function attachment( + mention: Mention | undefined, + content: string, + filename: string, +): FileAttachment | undefined { + if (!mention) return undefined + + return { + mime: "text/plain", + url: `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`, + filename, + source: { + type: "file", + path: filename, + text: mention, + }, + } +} diff --git a/packages/kilo-vscode/webview-ui/src/hooks/git-changes-context-utils.ts b/packages/kilo-vscode/webview-ui/src/hooks/git-changes-context-utils.ts index 2e83a8a80967..c115b820d9ed 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/git-changes-context-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/git-changes-context-utils.ts @@ -1,24 +1,14 @@ import type { FileAttachment } from "../types/messages" +import { attachment, find, type Mention } from "./context-mention-utils" export const GIT_CHANGES_MENTION = "git-changes" export const GIT_CHANGES_FILENAME = "git-changes.txt" export const GIT_CHANGES_PATTERN = /(^|\s)@git-changes(?=\s|$)/g -export type GitChangesMention = { - value: string - start: number - end: number -} +export type GitChangesMention = Mention export function findGitChangesMention(text: string): GitChangesMention | undefined { - GIT_CHANGES_PATTERN.lastIndex = 0 - const match = GIT_CHANGES_PATTERN.exec(text) - if (!match) return undefined - - const prefix = match[1] ?? "" - const start = match.index + prefix.length - const value = `@${GIT_CHANGES_MENTION}` - return { value, start, end: start + value.length } + return find(text, GIT_CHANGES_PATTERN, GIT_CHANGES_MENTION) } export function hasGitChangesMention(text: string): boolean { @@ -26,17 +16,5 @@ export function hasGitChangesMention(text: string): boolean { } export function buildGitChangesAttachment(text: string, content: string): FileAttachment | undefined { - const mention = findGitChangesMention(text) - if (!mention) return undefined - - return { - mime: "text/plain", - url: `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`, - filename: GIT_CHANGES_FILENAME, - source: { - type: "file", - path: GIT_CHANGES_FILENAME, - text: mention, - }, - } + return attachment(findGitChangesMention(text), content, GIT_CHANGES_FILENAME) } diff --git a/packages/kilo-vscode/webview-ui/src/hooks/terminal-context-utils.ts b/packages/kilo-vscode/webview-ui/src/hooks/terminal-context-utils.ts index fc0a5caaff4b..0152574dae2d 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/terminal-context-utils.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/terminal-context-utils.ts @@ -1,24 +1,14 @@ import type { FileAttachment } from "../types/messages" +import { attachment, find, type Mention } from "./context-mention-utils" export const TERMINAL_MENTION = "terminal" export const TERMINAL_FILENAME = "terminal-output.txt" export const TERMINAL_PATTERN = /(^|\s)@terminal(?=\s|$)/g -export type TerminalMention = { - value: string - start: number - end: number -} +export type TerminalMention = Mention export function findTerminalMention(text: string): TerminalMention | undefined { - TERMINAL_PATTERN.lastIndex = 0 - const match = TERMINAL_PATTERN.exec(text) - if (!match) return undefined - - const prefix = match[1] ?? "" - const start = match.index + prefix.length - const value = `@${TERMINAL_MENTION}` - return { value, start, end: start + value.length } + return find(text, TERMINAL_PATTERN, TERMINAL_MENTION) } export function hasTerminalMention(text: string): boolean { @@ -26,17 +16,5 @@ export function hasTerminalMention(text: string): boolean { } export function buildTerminalAttachment(text: string, content: string): FileAttachment | undefined { - const mention = findTerminalMention(text) - if (!mention) return undefined - - return { - mime: "text/plain", - url: `data:text/plain;charset=utf-8,${encodeURIComponent(content)}`, - filename: TERMINAL_FILENAME, - source: { - type: "file", - path: TERMINAL_FILENAME, - text: mention, - }, - } + return attachment(findTerminalMention(text), content, TERMINAL_FILENAME) }