From d9194633eb9fd06f159d289d6e93470d6b293e1a Mon Sep 17 00:00:00 2001 From: jana-hussien Date: Fri, 28 Aug 2026 11:32:06 +0000 Subject: [PATCH 1/2] Proj 1A --- .devcontainer/devcontainer.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..6eb442a1 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,11 @@ +{ + "name": "Default Linux Universal", + "image": "mcr.microsoft.com/devcontainers/javascript-node:22", + "features": { + "ghcr.io/devcontainer-community/devcontainer-features/bun.sh:1": {} + }, + "runArgs": [ + "--memory=4g", + "--memory-swap=4g" + ] +} \ No newline at end of file From c2a22b92a3661e1607cff3d8f88851f153222b9a Mon Sep 17 00:00:00 2001 From: jana-hussien Date: Sun, 6 Sep 2026 19:52:55 +0000 Subject: [PATCH 2/2] Refactor (packages/app/src/components/prompt-input/history.ts): Function with high complexity (count = 42) isPromptEqual had a length check, two manual for-loops, and a separate if-check for every prompt part type. Qlty flagged it for high complexity (count = 42). I pulled the per-type comparison logic out into PART_COMPARATORS, an object with one small function per part type ("text", "file", "agent", "image"). arePromptPartsEqual just looks up the right function by partA.type and calls it, instead of walking through an if-chain that checked types.length. File parts also needed to compare an optional selection object field by field, so that logic became its own isFileSelectionEqual helper rather than living inline inside the "file" branch. With the per-part checks factored out, the two manual for-loops (one over prompt parts, one over comments) could each be replaced by a single Array.every call, since all they were doing was checking every element pairwise and bailing on the first mismatch. Added tests for deduping file/agent/image parts, mismatched selections, mismatched part types, and mismatched comments, since the old tests only covered the text-part case. --- .../components/prompt-input/history.test.ts | 45 ++++++++++++++++ .../src/components/prompt-input/history.ts | 54 +++++++++---------- 2 files changed, 71 insertions(+), 28 deletions(-) diff --git a/packages/app/src/components/prompt-input/history.test.ts b/packages/app/src/components/prompt-input/history.test.ts index 94f24ce9..2159047d 100644 --- a/packages/app/src/components/prompt-input/history.test.ts +++ b/packages/app/src/components/prompt-input/history.test.ts @@ -41,6 +41,51 @@ describe("prompt-input history", () => { expect(dedupedComments).toBe(commentsOnly) }) + const filePrompt = (path: string, selection?: { startLine: number; startChar: number; endLine: number; endChar: number }): Prompt => [ + { type: "file", path, content: `@${path}`, start: 0, end: path.length + 1, selection }, + ] + const agentPrompt = (name: string): Prompt => [{ type: "agent", name, content: `@${name}`, start: 0, end: name.length + 1 }] + const imagePrompt = (id: string): Prompt => [ + { type: "image", id, filename: "img.png", mime: "image/png", blob: { id, url: `blob:${id}` } }, + ] + + test("prependHistoryEntry deduplicates file parts only when path and selection match", () => { + const selection = { startLine: 1, startChar: 0, endLine: 2, endChar: 3 } + const withFile = prependHistoryEntry([], filePrompt("src/a.ts", selection)) + expect(withFile).toHaveLength(1) + + const sameFile = prependHistoryEntry(withFile, filePrompt("src/a.ts", { ...selection })) + expect(sameFile).toBe(withFile) + + const differentPath = prependHistoryEntry(withFile, filePrompt("src/b.ts", selection)) + expect(differentPath).toHaveLength(2) + + const differentSelection = prependHistoryEntry(withFile, filePrompt("src/a.ts", { ...selection, endChar: 9 })) + expect(differentSelection).toHaveLength(2) + + const noSelection = prependHistoryEntry(withFile, filePrompt("src/a.ts", undefined)) + expect(noSelection).toHaveLength(2) + }) + + test("prependHistoryEntry deduplicates agent and image parts by identity", () => { + const withAgent = prependHistoryEntry([], agentPrompt("reviewer")) + expect(prependHistoryEntry(withAgent, agentPrompt("reviewer"))).toBe(withAgent) + expect(prependHistoryEntry(withAgent, agentPrompt("other"))).toHaveLength(2) + + const withImage = prependHistoryEntry([], imagePrompt("img-1")) + expect(prependHistoryEntry(withImage, imagePrompt("img-1"))).toBe(withImage) + expect(prependHistoryEntry(withImage, imagePrompt("img-2"))).toHaveLength(2) + }) + + test("prependHistoryEntry treats mismatched part types and comment fields as distinct entries", () => { + const withText = prependHistoryEntry([], text("hello")) + expect(prependHistoryEntry(withText, filePrompt("src/a.ts", undefined))).toHaveLength(2) + + const withComment = prependHistoryEntry([], text("hello"), [comment("c1")]) + const differentCommentText = prependHistoryEntry(withComment, text("hello"), [comment("c1", "different")]) + expect(differentCommentText).toHaveLength(2) + }) + test("navigatePromptHistory restores saved prompt when moving down from newest", () => { const entries = [text("third"), text("second"), text("first")] const up = navigatePromptHistory({ diff --git a/packages/app/src/components/prompt-input/history.ts b/packages/app/src/components/prompt-input/history.ts index 79e8abc0..3dff7910 100644 --- a/packages/app/src/components/prompt-input/history.ts +++ b/packages/app/src/components/prompt-input/history.ts @@ -112,39 +112,37 @@ function isCommentEqual(commentA: PromptHistoryComment, commentB: PromptHistoryC ) } +type PromptPart = Prompt[number] +type FilePromptPart = Extract + +function isFileSelectionEqual(a: FilePromptPart["selection"], b: FilePromptPart["selection"]) { + if (!a && !b) return true + if (!a || !b) return false + return a.startLine === b.startLine && a.startChar === b.startChar && a.endLine === b.endLine && a.endChar === b.endChar +} + +const PART_COMPARATORS: Record boolean> = { + text: (a, b) => a.type === "text" && b.type === "text" && a.content === b.content, + file: (a, b) => + a.type === "file" && b.type === "file" && a.path === b.path && isFileSelectionEqual(a.selection, b.selection), + agent: (a, b) => a.type === "agent" && b.type === "agent" && a.name === b.name, + image: (a, b) => a.type === "image" && b.type === "image" && a.id === b.id, +} + +function arePromptPartsEqual(partA: PromptPart, partB: PromptPart): boolean { + if (partA.type !== partB.type) return false + return PART_COMPARATORS[partA.type](partA, partB) +} + function isPromptEqual(promptA: PromptHistoryStoredEntry, promptB: PromptHistoryStoredEntry) { const entryA = normalizePromptHistoryEntry(promptA) const entryB = normalizePromptHistoryEntry(promptB) if (entryA.prompt.length !== entryB.prompt.length) return false - for (let i = 0; i < entryA.prompt.length; i++) { - const partA = entryA.prompt[i] - const partB = entryB.prompt[i] - if (partA.type !== partB.type) return false - if (partA.type === "text" && partA.content !== (partB.type === "text" ? partB.content : "")) return false - if (partA.type === "file") { - if (partA.path !== (partB.type === "file" ? partB.path : "")) return false - const a = partA.selection - const b = partB.type === "file" ? partB.selection : undefined - const sameSelection = - (!a && !b) || - (!!a && - !!b && - a.startLine === b.startLine && - a.startChar === b.startChar && - a.endLine === b.endLine && - a.endChar === b.endChar) - if (!sameSelection) return false - } - if (partA.type === "agent" && partA.name !== (partB.type === "agent" ? partB.name : "")) return false - if (partA.type === "image" && partA.id !== (partB.type === "image" ? partB.id : "")) return false - } if (entryA.comments.length !== entryB.comments.length) return false - for (let i = 0; i < entryA.comments.length; i++) { - const commentA = entryA.comments[i] - const commentB = entryB.comments[i] - if (!commentA || !commentB || !isCommentEqual(commentA, commentB)) return false - } - return true + return ( + entryA.prompt.every((part, i) => arePromptPartsEqual(part, entryB.prompt[i])) && + entryA.comments.every((comment, i) => isCommentEqual(comment, entryB.comments[i])) + ) } type HistoryNavInput = {