Skip to content

Commit 9bec497

Browse files
authored
feat: add posthog error tracking linking to array (#22)
1 parent e3f2d6b commit 9bec497

File tree

7 files changed

+482
-53
lines changed

7 files changed

+482
-53
lines changed

src/renderer/components/FileMentionList.tsx

Lines changed: 70 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Box, Flex, Text } from "@radix-ui/themes";
2+
import type { MentionItem } from "@shared/types";
23
import type { SuggestionKeyDownProps } from "@tiptap/suggestion";
34
import {
45
type ForwardedRef,
@@ -9,17 +10,22 @@ import {
910
useState,
1011
} from "react";
1112

12-
export interface FileMentionListProps {
13-
items: Array<{ path: string; name: string }>;
14-
command: (item: { id: string; label: string }) => void;
13+
export interface MentionListProps {
14+
items: MentionItem[];
15+
command: (item: {
16+
id: string;
17+
label: string;
18+
type?: string;
19+
urlId?: string;
20+
}) => void;
1521
}
1622

17-
export interface FileMentionListRef {
23+
export interface MentionListRef {
1824
onKeyDown: (props: SuggestionKeyDownProps) => boolean;
1925
}
2026

21-
export const FileMentionList = forwardRef(
22-
(props: FileMentionListProps, ref: ForwardedRef<FileMentionListRef>) => {
27+
export const MentionList = forwardRef(
28+
(props: MentionListProps, ref: ForwardedRef<MentionListRef>) => {
2329
const [selectedIndex, setSelectedIndex] = useState(0);
2430
const containerRef = useRef<HTMLDivElement>(null);
2531
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
@@ -48,7 +54,22 @@ export const FileMentionList = forwardRef(
4854
const selectItem = (index: number) => {
4955
const item = props.items[index];
5056
if (item) {
51-
props.command({ id: item.path, label: item.name });
57+
if (item.path) {
58+
// File item
59+
props.command({
60+
id: item.path,
61+
label: item.name || item.path.split("/").pop() || item.path,
62+
type: "file",
63+
});
64+
} else if (item.url) {
65+
// URL item
66+
props.command({
67+
id: item.url,
68+
label: item.label || item.url,
69+
type: item.type || "generic",
70+
urlId: item.urlId,
71+
});
72+
}
5273
}
5374
};
5475

@@ -131,24 +152,51 @@ export const FileMentionList = forwardRef(
131152
overflow: "auto",
132153
}}
133154
>
134-
{props.items.map((item, index) => (
135-
<Flex
136-
key={item.path}
137-
ref={(el) => {
138-
itemRefs.current[index] = el;
139-
}}
140-
className={`file-mention-item ${index === selectedIndex ? "is-selected" : ""}`}
141-
onClick={() => selectItem(index)}
142-
onMouseEnter={() => setSelectedIndex(index)}
143-
>
144-
<Flex direction="column" gap="1">
145-
<Text size="1">{item.path}</Text>
155+
{props.items.map((item, index) => {
156+
const key = item.path || item.url || `item-${index}`;
157+
const displayText = item.path
158+
? item.path
159+
: item.label || item.url || "Unknown item";
160+
const itemType = item.type === "file" ? "File" : item.type || "URL";
161+
162+
return (
163+
<Flex
164+
key={key}
165+
ref={(el) => {
166+
itemRefs.current[index] = el;
167+
}}
168+
className={`file-mention-item ${index === selectedIndex ? "is-selected" : ""}`}
169+
onClick={() => selectItem(index)}
170+
onMouseEnter={() => setSelectedIndex(index)}
171+
style={{
172+
padding: "var(--space-2)",
173+
cursor: "pointer",
174+
backgroundColor:
175+
index === selectedIndex ? "var(--gray-3)" : "transparent",
176+
color:
177+
index === selectedIndex ? "var(--gray-12)" : "var(--gray-11)",
178+
borderRadius: "var(--radius-1)",
179+
}}
180+
>
181+
<Flex direction="column" gap="1">
182+
<Text size="2" weight="medium">
183+
{displayText}
184+
</Text>
185+
{item.type && item.type !== "file" && (
186+
<Text size="1">{itemType}</Text>
187+
)}
188+
</Flex>
146189
</Flex>
147-
</Flex>
148-
))}
190+
);
191+
})}
149192
</Box>
150193
);
151194
},
152195
);
153196

154-
FileMentionList.displayName = "FileMentionList";
197+
MentionList.displayName = "MentionList";
198+
199+
// Backward compatibility export
200+
export const FileMentionList = MentionList;
201+
export type FileMentionListRef = MentionListRef;
202+
export type FileMentionListProps = MentionListProps;

src/renderer/components/RichTextEditor.tsx

Lines changed: 160 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import type { SuggestionOptions } from "@tiptap/suggestion";
1010
import { useEffect, useRef, useState } from "react";
1111
import { createRoot } from "react-dom/client";
1212
import { markdownToTiptap, tiptapToMarkdown } from "../utils/tiptap-converter";
13-
import { FileMentionList, type FileMentionListRef } from "./FileMentionList";
13+
import { parsePostHogUrl, isUrl, extractUrlFromMarkdown } from "../utils/posthog-url-parser";
14+
import { MentionList, type MentionListRef } from "./FileMentionList";
1415
import { FormattingToolbar } from "./FormattingToolbar";
16+
import type { MentionItem } from "@shared/types";
1517

1618
interface RichTextEditorProps {
1719
value: string;
@@ -38,15 +40,15 @@ export function RichTextEditor({
3840
showToolbar = true,
3941
minHeight = "100px",
4042
}: RichTextEditorProps) {
41-
const [files, setFiles] = useState<Array<{ path: string; name: string }>>([]);
42-
const filesRef = useRef(files);
43+
const [mentionItems, setMentionItems] = useState<MentionItem[]>([]);
44+
const mentionItemsRef = useRef(mentionItems);
4345
const onChangeRef = useRef(onChange);
4446
const repoPathRef = useRef(repoPath);
4547

4648
// Keep refs updated
4749
useEffect(() => {
48-
filesRef.current = files;
49-
}, [files]);
50+
mentionItemsRef.current = mentionItems;
51+
}, [mentionItems]);
5052

5153
useEffect(() => {
5254
onChangeRef.current = onChange;
@@ -75,26 +77,116 @@ export function RichTextEditor({
7577
Placeholder.configure({
7678
placeholder,
7779
}),
78-
Mention.configure({
80+
Mention.extend({
81+
addAttributes() {
82+
return {
83+
id: {
84+
default: null,
85+
},
86+
label: {
87+
default: null,
88+
},
89+
type: {
90+
default: 'file',
91+
},
92+
urlId: {
93+
default: null,
94+
},
95+
};
96+
},
97+
renderText({ node }) {
98+
// Use the label for display, fallback to id
99+
return `@${node.attrs.label || node.attrs.id}`;
100+
},
101+
}).configure({
79102
HTMLAttributes: {
80103
class: "file-mention",
81104
},
82105
suggestion: {
106+
char: '@',
107+
allowSpaces: true,
108+
command: ({ editor, range, props }) => {
109+
// Insert mention with all attributes
110+
const nodeAfter = editor.view.state.selection.$to.nodeAfter;
111+
const overrideSpace = nodeAfter?.text?.startsWith(' ');
112+
113+
if (overrideSpace) {
114+
range.to += 1;
115+
}
116+
117+
editor
118+
.chain()
119+
.focus()
120+
.insertContentAt(range, [
121+
{
122+
type: 'mention',
123+
attrs: {
124+
id: props.id,
125+
label: props.label,
126+
type: props.type || 'file',
127+
urlId: props.urlId,
128+
},
129+
},
130+
{
131+
type: 'text',
132+
text: ' ',
133+
},
134+
])
135+
.run();
136+
},
83137
items: async ({ query }: { query: string }) => {
84-
if (!repoPathRef.current) return [];
138+
const items: MentionItem[] = [];
139+
140+
// Extract URL from markdown link syntax if present: [text](url)
141+
const urlToCheck = extractUrlFromMarkdown(query);
142+
143+
// Check if the query looks like a URL
144+
if (isUrl(urlToCheck)) {
145+
const postHogInfo = parsePostHogUrl(urlToCheck);
146+
if (postHogInfo) {
147+
// It's a PostHog URL
148+
items.push({
149+
url: urlToCheck,
150+
type: postHogInfo.type,
151+
label: postHogInfo.label,
152+
id: postHogInfo.id,
153+
urlId: postHogInfo.id,
154+
});
155+
} else {
156+
// It's a generic URL
157+
try {
158+
const urlObj = new URL(urlToCheck);
159+
items.push({
160+
url: urlToCheck,
161+
type: 'generic',
162+
label: urlObj.hostname,
163+
});
164+
} catch {
165+
// Invalid URL, ignore
166+
}
167+
}
168+
}
85169

86-
try {
87-
const results = await window.electronAPI?.listRepoFiles(
88-
repoPathRef.current,
89-
query,
90-
);
91-
const fileList = results || [];
92-
setFiles(fileList);
93-
return fileList;
94-
} catch (error) {
95-
console.error("Error fetching files:", error);
96-
return [];
170+
// Only search for files if we haven't detected a URL
171+
if (repoPathRef.current && query.length > 0 && !isUrl(urlToCheck)) {
172+
try {
173+
const results = await window.electronAPI?.listRepoFiles(
174+
repoPathRef.current,
175+
query,
176+
);
177+
const fileItems = (results || []).map((file) => ({
178+
path: file.path,
179+
name: file.name,
180+
type: 'file' as const,
181+
}));
182+
items.push(...fileItems);
183+
} catch (error) {
184+
console.error("Error fetching files:", error);
185+
}
97186
}
187+
188+
setMentionItems(items);
189+
return items;
98190
},
99191
render: () => {
100192
let component: { destroy: () => void } | null = null;
@@ -123,13 +215,13 @@ export function RichTextEditor({
123215
},
124216
};
125217

126-
const ref: { current: FileMentionListRef | null } = {
218+
const ref: { current: MentionListRef | null } = {
127219
current: null,
128220
};
129221

130222
root.render(
131-
<FileMentionList
132-
items={filesRef.current}
223+
<MentionList
224+
items={mentionItemsRef.current}
133225
command={props.command}
134226
ref={(instance) => {
135227
ref.current = instance;
@@ -144,13 +236,13 @@ export function RichTextEditor({
144236
onUpdate: (props: any) => {
145237
if (!root) return;
146238

147-
const ref: { current: FileMentionListRef | null } = {
239+
const ref: { current: MentionListRef | null } = {
148240
current: null,
149241
};
150242

151243
root.render(
152-
<FileMentionList
153-
items={filesRef.current}
244+
<MentionList
245+
items={mentionItemsRef.current}
154246
command={props.command}
155247
ref={(instance) => {
156248
ref.current = instance;
@@ -208,6 +300,50 @@ export function RichTextEditor({
208300
class: "rich-text-editor-content",
209301
style: `outline: none; min-height: ${minHeight}; padding: var(--space-3);`,
210302
},
303+
handlePaste: (view, event) => {
304+
// Check if we're in a mention context (text before cursor has @)
305+
const { state } = view;
306+
const { selection } = state;
307+
const { $from } = selection;
308+
309+
// Get text before cursor in current paragraph
310+
const textBefore = $from.parent.textBetween(
311+
Math.max(0, $from.parentOffset - 500),
312+
$from.parentOffset,
313+
undefined,
314+
'\ufffc'
315+
);
316+
317+
// Check if there's an @ symbol before the cursor without any whitespace before it
318+
// or if @ is the last character before cursor (just typed @)
319+
const lastAtIndex = textBefore.lastIndexOf('@');
320+
if (lastAtIndex !== -1) {
321+
const textAfterAt = textBefore.substring(lastAtIndex + 1);
322+
323+
// We're in mention mode if:
324+
// 1. There's no space between @ and cursor, OR
325+
// 2. @ is immediately before cursor
326+
if (!textAfterAt.includes(' ') || lastAtIndex === textBefore.length - 1) {
327+
const clipboardData = event.clipboardData;
328+
if (clipboardData) {
329+
const pastedText = clipboardData.getData('text/plain').trim();
330+
331+
// If pasted content is a URL, prevent default paste and insert as plain text
332+
if (pastedText && isUrl(pastedText)) {
333+
event.preventDefault();
334+
335+
// Insert the URL as plain text to trigger mention suggestion
336+
const transaction = state.tr.insertText(pastedText);
337+
view.dispatch(transaction);
338+
339+
return true;
340+
}
341+
}
342+
}
343+
}
344+
345+
return false;
346+
},
211347
handleKeyDown: (_view, event) => {
212348
// Custom keyboard shortcuts
213349
if (event.ctrlKey || event.metaKey) {

src/renderer/components/TaskCreate.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export function TaskCreate({ open, onOpenChange }: TaskCreateProps) {
5050
const { data: repositories = [] } = useRepositories(githubIntegration?.id);
5151
const [isExpanded, setIsExpanded] = useState(false);
5252
const [createMore, setCreateMore] = useState(false);
53+
const [_repoPath, setRepoPath] = useState<string | null>(null);
5354

5455
const defaultWorkflow = useMemo(
5556
() => workflows.find((w) => w.is_active && w.is_default) || workflows[0],
@@ -129,6 +130,7 @@ export function TaskCreate({ open, onOpenChange }: TaskCreateProps) {
129130
data: newTask,
130131
});
131132
reset();
133+
setRepoPath(null); // Reset the local repo path for next task
132134
if (!createMore) {
133135
onOpenChange(false);
134136
}

src/renderer/styles/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ body {
4040
.file-mention-item {
4141
padding: var(--space-2);
4242
cursor: pointer;
43-
background-color: transparent;
43+
background-color: white;
4444
border-radius: var(--radius-2);
4545
transition: background-color 0.1s ease;
4646
}

0 commit comments

Comments
 (0)