diff --git a/.changeset/early-rice-guess.md b/.changeset/early-rice-guess.md new file mode 100644 index 00000000000..5423263704d --- /dev/null +++ b/.changeset/early-rice-guess.md @@ -0,0 +1,5 @@ +--- +"kilo-code": patch +--- + +Fix BrowserSessionRow crash on non-string inputs diff --git a/src/shared/browserUtils.ts b/src/shared/browserUtils.ts index 4e071121c1b..c4ce4d479b7 100644 --- a/src/shared/browserUtils.ts +++ b/src/shared/browserUtils.ts @@ -36,6 +36,7 @@ export function scaleCoordinate(coordinate: string, viewportWidth: number, viewp */ export function prettyKey(k?: string): string { if (!k) return "" + if (typeof k !== "string") return String(k) return k .split("+") .map((part) => { @@ -69,7 +70,8 @@ export function prettyKey(k?: string): string { if (keyMatch) return keyMatch[1].toUpperCase() const digitMatch = /^Digit([0-9])$/.exec(p) if (digitMatch) return digitMatch[1] - const spaced = p.replace(/([a-z])([A-Z])/g, "$1 $2") + // Guard against non-string p (though unlikely from split) to be safe + const spaced = typeof p === "string" ? p.replace(/([a-z])([A-Z])/g, "$1 $2") : String(p || "") // kilocode_change return spaced.charAt(0).toUpperCase() + spaced.slice(1) }) .join(" + ") diff --git a/webview-ui/src/components/chat/BrowserSessionRow.tsx b/webview-ui/src/components/chat/BrowserSessionRow.tsx index 5255ac2129a..f8adbfe093f 100644 --- a/webview-ui/src/components/chat/BrowserSessionRow.tsx +++ b/webview-ui/src/components/chat/BrowserSessionRow.tsx @@ -69,7 +69,9 @@ const getBrowserActionText = ( coordinate: executedCoordinate || getViewportCoordinate(coordinate), }) case "resize": - return t("chat:browser.actions.resized", { size: size?.split(/[x,]/).join(" x ") }) + return t("chat:browser.actions.resized", { + size: typeof size === "string" ? size.split(/[x,]/).join(" x ") : String(size || ""), // kilocode_change + }) case "screenshot": return t("chat:browser.actions.screenshotSaved") case "close": diff --git a/webview-ui/src/components/chat/__tests__/BrowserSessionRowCrash.spec.tsx b/webview-ui/src/components/chat/__tests__/BrowserSessionRowCrash.spec.tsx new file mode 100644 index 00000000000..0a7c843981c --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/BrowserSessionRowCrash.spec.tsx @@ -0,0 +1,145 @@ +import { render } from "@testing-library/react" +import BrowserSessionRow from "../BrowserSessionRow" +import { ClineMessage } from "@roo-code/types" +import { TooltipProvider } from "@src/components/ui/tooltip" + +// Mock dependencies +vi.mock("react-i18next", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + useTranslation: () => ({ + t: (key: string, options?: any) => { + if (key === "chat:browser.actions.clicked" && options?.coordinate) { + return `Clicked ${options.coordinate}` + } + if (key === "chat:browser.actions.pressed" && options?.key) { + return `Pressed ${options.key}` + } + if (key === "chat:browser.actions.typed" && options?.text) { + return `Typed ${options.text}` + } + if (key === "chat:browser.actions.resized" && options?.size) { + return `Resized to ${options.size}` // Changed to expect formatted size + } + + return key + }, + }), + initReactI18next: { + type: "3rdParty", + init: () => {}, + }, + } +}) + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: vi.fn(), + }, +})) + +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + browserViewportSize: "900x600", + isBrowserSessionActive: true, + }), +})) + +vi.mock("../kilocode/common/CodeBlock", () => ({ + default: ({ source }: { source: string }) =>
{source}
, +})) + +describe("BrowserSessionRow Crash Reproduction", () => { + it("should handle non-string text in press action", () => { + const invalidPressPayload = { + action: "press", + // @ts-expect-error - Simulating invalid type + text: 12345, + } satisfies { action: "press"; text: string } + + const messages: ClineMessage[] = [ + { + ts: 1234567890, + type: "say", + say: "browser_action", + text: JSON.stringify({ + ...invalidPressPayload, + }), + }, + { + ts: 1234567891, + type: "say", + say: "browser_action_result", + text: JSON.stringify({ + currentUrl: "http://example.com", + logs: "", + screenshot: "", + currentMousePosition: "", + viewportWidth: 900, + viewportHeight: 600, + }), + }, + ] + + expect(() => { + render( + + true} + onToggleExpand={() => {}} + isLast={true} + isStreaming={false} + /> + , + ) + }).not.toThrow() + }) + + it("should handle non-string size in resize action", () => { + const invalidResizePayload = { + action: "resize", + // @ts-expect-error - Simulating invalid type + size: 12345, + } satisfies { action: "resize"; size: string } + + const messages: ClineMessage[] = [ + { + ts: 1234567892, + type: "say", + say: "browser_action", + text: JSON.stringify({ + ...invalidResizePayload, + }), + }, + { + ts: 1234567893, + type: "say", + say: "browser_action_result", + text: JSON.stringify({ + currentUrl: "http://example.com", + logs: "", + screenshot: "", + currentMousePosition: "", + viewportWidth: 900, + viewportHeight: 600, + }), + }, + ] + + expect(() => { + render( + + true} + onToggleExpand={() => {}} + isLast={true} + isStreaming={false} + /> + , + ) + }).not.toThrow() + }) +})