Skip to content

Commit 8912d7e

Browse files
committed
Support files outside the repo in panels and prompt builder
1 parent 7e1857f commit 8912d7e

File tree

7 files changed

+114
-22
lines changed

7 files changed

+114
-22
lines changed

apps/twig/src/main/services/fs/schemas.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export const readRepoFileInput = z.object({
1111
filePath: z.string(),
1212
});
1313

14+
export const readAbsoluteFileInput = z.object({
15+
filePath: z.string(),
16+
});
17+
1418
export const writeRepoFileInput = z.object({
1519
repoPath: z.string(),
1620
filePath: z.string(),

apps/twig/src/main/services/fs/service.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,17 @@ export class FsService {
9797
}
9898
}
9999

100+
async readAbsoluteFile(filePath: string): Promise<string | null> {
101+
try {
102+
return await fs.promises.readFile(path.resolve(filePath), "utf-8");
103+
} catch (error) {
104+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
105+
log.error(`Failed to read file ${filePath}:`, error);
106+
}
107+
return null;
108+
}
109+
}
110+
100111
async writeRepoFile(
101112
repoPath: string,
102113
filePath: string,

apps/twig/src/main/trpc/routers/fs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { MAIN_TOKENS } from "../../di/tokens.js";
33
import {
44
listRepoFilesInput,
55
listRepoFilesOutput,
6+
readAbsoluteFileInput,
67
readRepoFileInput,
78
readRepoFileOutput,
89
writeRepoFileInput,
@@ -27,6 +28,11 @@ export const fsRouter = router({
2728
getService().readRepoFile(input.repoPath, input.filePath),
2829
),
2930

31+
readAbsoluteFile: publicProcedure
32+
.input(readAbsoluteFileInput)
33+
.output(readRepoFileOutput)
34+
.query(({ input }) => getService().readAbsoluteFile(input.filePath)),
35+
3036
writeRepoFile: publicProcedure
3137
.input(writeRepoFileInput)
3238
.mutation(({ input }) =>

apps/twig/src/renderer/components/ui/PanelMessage.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,32 @@ import { Box, Flex, Text } from "@radix-ui/themes";
22

33
interface PanelMessageProps {
44
children: React.ReactNode;
5+
detail?: string;
56
color?: "gray" | "red";
67
}
78

8-
export function PanelMessage({ children, color = "gray" }: PanelMessageProps) {
9+
export function PanelMessage({
10+
children,
11+
detail,
12+
color = "gray",
13+
}: PanelMessageProps) {
914
return (
1015
<Box height="100%" p="4">
11-
<Flex align="center" justify="center" height="100%">
16+
<Flex
17+
align="center"
18+
justify="center"
19+
direction="column"
20+
gap="1"
21+
height="100%"
22+
>
1223
<Text size="2" color={color}>
1324
{children}
1425
</Text>
26+
{detail && (
27+
<Text size="1" color="gray" trim="both">
28+
{detail}
29+
</Text>
30+
)}
1531
</Flex>
1632
</Box>
1733
);

apps/twig/src/renderer/features/code-editor/components/CodeEditorPanel.tsx

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,26 @@ import { PanelMessage } from "@components/ui/PanelMessage";
22
import { CodeMirrorEditor } from "@features/code-editor/components/CodeMirrorEditor";
33
import { getRelativePath } from "@features/code-editor/utils/pathUtils";
44
import { useCwd } from "@features/sidebar/hooks/useCwd";
5-
import { Box } from "@radix-ui/themes";
5+
import { Box, Flex } from "@radix-ui/themes";
66
import { trpcReact } from "@renderer/trpc/client";
77
import type { Task } from "@shared/types";
88

9+
const IMAGE_EXTENSIONS = new Set([
10+
"png",
11+
"jpg",
12+
"jpeg",
13+
"gif",
14+
"webp",
15+
"svg",
16+
"bmp",
17+
"ico",
18+
]);
19+
20+
function isImageFile(filePath: string): boolean {
21+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
22+
return IMAGE_EXTENSIONS.has(ext);
23+
}
24+
925
interface CodeEditorPanelProps {
1026
taskId: string;
1127
task: Task;
@@ -18,20 +34,42 @@ export function CodeEditorPanel({
1834
absolutePath,
1935
}: CodeEditorPanelProps) {
2036
const repoPath = useCwd(taskId);
37+
const isInsideRepo = !!repoPath && absolutePath.startsWith(repoPath);
2138
const filePath = getRelativePath(absolutePath, repoPath);
39+
const isImage = isImageFile(absolutePath);
40+
41+
const repoQuery = trpcReact.fs.readRepoFile.useQuery(
42+
{ repoPath: repoPath ?? "", filePath },
43+
{ enabled: isInsideRepo && !isImage, staleTime: Infinity },
44+
);
2245

23-
const {
24-
data: fileContent,
25-
isLoading,
26-
error,
27-
} = trpcReact.fs.readRepoFile.useQuery(
28-
{ repoPath: repoPath ?? "", filePath: filePath ?? "" },
29-
{
30-
enabled: !!repoPath && !!filePath,
31-
staleTime: Infinity,
32-
},
46+
const absoluteQuery = trpcReact.fs.readAbsoluteFile.useQuery(
47+
{ filePath: absolutePath },
48+
{ enabled: !isInsideRepo && !isImage, staleTime: Infinity },
3349
);
3450

51+
const { data: fileContent, isLoading, error } = isInsideRepo
52+
? repoQuery
53+
: absoluteQuery;
54+
55+
if (isImage) {
56+
return (
57+
<Flex
58+
align="center"
59+
justify="center"
60+
height="100%"
61+
p="4"
62+
style={{ overflow: "auto" }}
63+
>
64+
<img
65+
src={`file://${absolutePath}`}
66+
alt={filePath}
67+
style={{ maxWidth: "100%", maxHeight: "100%", objectFit: "contain" }}
68+
/>
69+
</Flex>
70+
);
71+
}
72+
3573
if (!repoPath) {
3674
return <PanelMessage>No repository path available</PanelMessage>;
3775
}
@@ -41,7 +79,9 @@ export function CodeEditorPanel({
4179
}
4280

4381
if (error || fileContent == null) {
44-
return <PanelMessage>Failed to load file</PanelMessage>;
82+
return (
83+
<PanelMessage detail={absolutePath}>Failed to load file</PanelMessage>
84+
);
4585
}
4686

4787
// If we ever allow editing in the CodeMirrorEditor, this can be removed

apps/twig/src/renderer/features/editor/utils/prompt-builder.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ function getMimeType(filePath: string): string {
1818
return mimeTypes[ext ?? ""] ?? "text/plain";
1919
}
2020

21+
function isAbsolutePath(filePath: string): boolean {
22+
return filePath.startsWith("/") || /^[a-zA-Z]:\\/.test(filePath);
23+
}
24+
25+
async function readFileContent(
26+
filePath: string,
27+
repoPath: string,
28+
): Promise<string | null> {
29+
if (isAbsolutePath(filePath)) {
30+
return trpcVanilla.fs.readAbsoluteFile.query({ filePath });
31+
}
32+
return trpcVanilla.fs.readRepoFile.query({ repoPath, filePath });
33+
}
34+
2135
export async function buildPromptBlocks(
2236
textContent: string,
2337
filePaths: string[],
@@ -41,18 +55,18 @@ export async function buildPromptBlocks(
4155

4256
blocks.push({ type: "text", text: textContent });
4357

44-
for (const relativePath of filePaths) {
58+
for (const filePath of filePaths) {
4559
try {
46-
const fileContent = await trpcVanilla.fs.readRepoFile.query({
47-
repoPath,
48-
filePath: relativePath,
49-
});
60+
const fileContent = await readFileContent(filePath, repoPath);
5061
if (fileContent) {
62+
const uri = isAbsolutePath(filePath)
63+
? `file://${filePath}`
64+
: `file://${repoPath}/${filePath}`;
5165
blocks.push({
5266
type: "resource",
5367
resource: {
54-
uri: `file://${repoPath}/${relativePath}`,
55-
mimeType: getMimeType(relativePath),
68+
uri,
69+
mimeType: getMimeType(filePath),
5670
text: fileContent,
5771
},
5872
});

apps/twig/src/renderer/features/panels/hooks/usePanelLayoutHooks.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,8 @@ export function useTabInjection(
8484
tabs.map((tab) => {
8585
let updatedData = tab.data;
8686
if (tab.data.type === "file" || tab.data.type === "diff") {
87-
const absolutePath = `${repoPath}/${tab.data.relativePath}`;
87+
const rp = tab.data.relativePath;
88+
const absolutePath = rp.startsWith("/") ? rp : `${repoPath}/${rp}`;
8889
updatedData = {
8990
...tab.data,
9091
absolutePath,

0 commit comments

Comments
 (0)