Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/apply-patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,13 +191,16 @@ assert.throws(

const overwriteRoot = await mkdtemp(join(tmpdir(), "devspace-apply-patch-overwrite-"));
await writeFile(join(overwriteRoot, "duplicate.txt"), "old content\n");
await applyPatch(
const overwriteResult = await applyPatch(
overwriteRoot,
`*** Begin Patch
*** Add File: duplicate.txt
+new content
*** End Patch`,
);
assert.deepEqual(overwriteResult.files, [
{ path: "duplicate.txt", operation: "update" },
]);
assert.equal(await readFile(join(overwriteRoot, "duplicate.txt"), "utf8"), "new content\n");

await writeFile(join(overwriteRoot, "source.txt"), "from\n");
Expand Down
2 changes: 1 addition & 1 deletion src/apply-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ export async function applyPatch(root: string, patch: string): Promise<ApplyPatc
const original = await readStagedOptional(absolute, action.path);
staged.set(absolute, { content: action.content, mode: original?.mode });
patches.push(unifiedFilePatch(action.path, action.path, original?.content ?? null, action.content));
results.push({ path: action.path, operation: "add" });
results.push({ path: action.path, operation: original ? "update" : "add" });
continue;
}

Expand Down
37 changes: 37 additions & 0 deletions src/ui/card-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import test from "node:test";
import {
isEditTool,
isExpandableCard,
isInitiallyExpandedCard,
isPatchTool,
isShellTool,
isToolName,
Expand Down Expand Up @@ -31,6 +32,42 @@ test("a patch card expands only when it contains patch content", () => {
assert.equal(isExpandableCard({ tool: "apply_patch" }), false);
});

test("a single-file patch opens immediately", () => {
assert.equal(
isInitiallyExpandedCard({
tool: "apply_patch",
files: [{ path: "src/a.ts", operation: "update" }],
payload: { patch: "diff --git a/src/a.ts b/src/a.ts" },
}),
true,
);
});

test("a multi-file patch stays collapsed", () => {
assert.equal(
isInitiallyExpandedCard({
tool: "apply_patch",
files: [
{ path: "src/a.ts", operation: "update" },
{ path: "src/b.ts", operation: "add" },
],
payload: { patch: "diff --git a/src/a.ts b/src/a.ts" },
}),
false,
);
});

test("show changes still opens immediately", () => {
assert.equal(
isInitiallyExpandedCard({
tool: "show_changes",
files: [{ path: "src/a.ts", type: "change" }],
payload: { patch: "diff --git a/src/a.ts b/src/a.ts" },
}),
true,
);
});

test("a workspace card expands when it contains provider metadata", () => {
assert.equal(
isExpandableCard({
Expand Down
16 changes: 15 additions & 1 deletion src/ui/card-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export type ToolName =
export type HostContext = NonNullable<ReturnType<App["getHostContext"]>>;

export type PatchOperation = "add" | "update" | "delete" | "move";
export type ReviewFileType =
| "change"
| "rename-pure"
| "rename-changed"
| "new"
| "deleted";

export interface ToolResultCard {
tool: ToolName;
Expand All @@ -41,7 +47,7 @@ export interface ToolResultCard {
path?: string;
previousPath?: string;
operation?: PatchOperation;
type?: string;
type?: ReviewFileType;
additions?: number;
removals?: number;
}>;
Expand Down Expand Up @@ -182,3 +188,11 @@ export function isExpandableCard(card: ToolResultCard): boolean {

return Boolean(card.payload);
}

export function isInitiallyExpandedCard(card: ToolResultCard): boolean {
if (isReviewTool(card.tool)) return isExpandableCard(card);
if (isPatchTool(card.tool)) {
return card.files?.length === 1 && isExpandableCard(card);
}
return false;
}
9 changes: 6 additions & 3 deletions src/ui/heavy-payload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
type HostContext,
type ToolResultCard,
} from "./card-types.js";
import { pierrePrettyScrollbarCss } from "./scrollbar.js";

type ThemeType = "light" | "dark";

Expand Down Expand Up @@ -77,7 +78,7 @@ function HeavyPayload({
);
}

return <pre className={`text-payload ${card.tool}`}>{text}</pre>;
return <pre className={`text-payload pretty-scrollbar ${card.tool}`}>{text}</pre>;
}

function FilePayload({
Expand All @@ -100,6 +101,7 @@ function FilePayload({
},
themeType,
overflow: "scroll",
unsafeCSS: pierrePrettyScrollbarCss,
}),
[themeType],
);
Expand Down Expand Up @@ -134,7 +136,7 @@ function FilePayload({
};
}, [fileOptions, path, startLine, text]);

return <div ref={wrapperRef} className="pierre-file" />;
return <div ref={wrapperRef} className="pierre-file pretty-scrollbar" />;
}

function DiffPayload({
Expand All @@ -158,12 +160,13 @@ function DiffPayload({
hunkSeparators: "line-info",
lineDiffType: "word-alt",
overflow: "scroll",
unsafeCSS: pierrePrettyScrollbarCss,
collapsedContextThreshold: 4,
expansionLineCount: 20,
stickyHeader: true,
disableFileHeader: true,
}}
className="pierre-diff"
className="pierre-diff pretty-scrollbar"
/>
);
}
Expand Down
18 changes: 18 additions & 0 deletions src/ui/icons.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import {
Blocks,
Bot,
ChevronDown,
CircleAlert,
Cpu,
FileDiff,
FileCheck2,
FileMinus,
FilePenLine,
FilePlus,
FileText,
Files,
FolderGit2,
FolderOpen,
FolderTree,
GitBranch,
GitCommitHorizontal,
LoaderCircle,
Search,
SquareTerminal,
Expand All @@ -17,18 +25,28 @@ import {
} from "lucide";

export const toolIcons = {
agents: Bot,
base: GitCommitHorizontal,
chevronDown: ChevronDown,
deleteFile: FileMinus,
diff: FileDiff,
editFile: FilePenLine,
files: Files,
folderOpen: FolderOpen,
folderTree: FolderTree,
gitBranch: GitBranch,
instructions: FileText,
instructionAvailable: FileText,
instructionLoaded: FileCheck2,
loading: LoaderCircle,
providers: Cpu,
readFile: FileText,
search: Search,
skills: Blocks,
sourceCheckout: FolderGit2,
terminal: Terminal,
terminalSquare: SquareTerminal,
warning: CircleAlert,
writeFile: FilePlus,
} as const satisfies Record<string, IconNode>;

Expand Down
125 changes: 122 additions & 3 deletions src/ui/patch-display.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import assert from "node:assert/strict";
import { getPatchDisplayParts } from "./patch-display.js";
import {
getFileChangePathDisplay,
getPatchDisplayParts,
getRenderedFileChangeKind,
getRenderedFileChangePathDisplay,
} from "./patch-display.js";

assert.deepEqual(getPatchDisplayParts({}), {
title: "Applied patch",
Expand All @@ -10,7 +15,7 @@ assert.deepEqual(
getPatchDisplayParts({ files: [{ path: "created.ts", operation: "add" }] }),
{
title: "Added 1 file",
iconOperation: "add",
iconKind: "added",
tone: "write",
},
);
Expand All @@ -24,11 +29,125 @@ assert.deepEqual(
}),
{
title: "Added 2 files",
iconOperation: "add",
iconKind: "added",
tone: "write",
},
);

assert.deepEqual(
getFileChangePathDisplay({
path: "src/new-name.ts",
previousPath: "src/old-name.ts",
}),
{
current: "new-name.ts",
previous: "old-name.ts",
title: "src/old-name.ts → src/new-name.ts",
},
);

assert.deepEqual(
getFileChangePathDisplay({
path: "packages/new/file.ts",
previousPath: "src/old/file.ts",
}),
{
current: "packages/new/file.ts",
previous: "src/old/file.ts",
title: "src/old/file.ts → packages/new/file.ts",
},
);

assert.deepEqual(
getRenderedFileChangePathDisplay(
[{ path: "src/new-name.ts", previousPath: "src/old-name.ts", operation: "move" }],
{ path: "src/new-name.ts" },
0,
),
{
current: "new-name.ts",
previous: "old-name.ts",
title: "src/old-name.ts → src/new-name.ts",
},
);

assert.equal(
getRenderedFileChangeKind(
[
{ path: "same.tmp", operation: "add" },
{ path: "same.tmp", operation: "delete" },
],
{ path: "same.tmp", type: "new" },
0,
),
"added",
);

assert.equal(
getRenderedFileChangeKind(
[
{ path: "same.tmp", operation: "add" },
{ path: "same.tmp", operation: "delete" },
],
{ path: "same.tmp", type: "deleted" },
1,
),
"deleted",
);

assert.equal(
getRenderedFileChangeKind(
[{ path: "report.md", operation: "add" }],
{ path: "report.md", type: "change" },
0,
),
"edited",
);

assert.equal(
getRenderedFileChangeKind(
[{ path: "renamed.md", previousPath: "old.md", operation: "move" }],
{ path: "renamed.md", type: "change" },
0,
),
"renamed",
);

assert.deepEqual(
getPatchDisplayParts({ files: [{ path: "created.ts", type: "new" }] }),
{
title: "Added 1 file",
iconKind: "added",
tone: "write",
},
);

assert.deepEqual(
getPatchDisplayParts({ files: [{ path: "renamed.ts", type: "rename-changed" }] }),
{
title: "Renamed and edited 1 file",
iconKind: "renamed-edited",
tone: "edit",
},
);

assert.deepEqual(
getPatchDisplayParts({ files: [{ path: "removed.ts", type: "deleted" }] }),
{
title: "Deleted 1 file",
iconKind: "deleted",
tone: "delete",
},
);

assert.deepEqual(
getPatchDisplayParts({ files: [{ path: "unknown.ts" }] }),
{
title: "Changed 1 file",
tone: "edit",
},
);

assert.deepEqual(
getPatchDisplayParts({
files: [
Expand Down
Loading
Loading