From 4ce1bd4ffab2fd0df155de6ea3bfee85a1603acc Mon Sep 17 00:00:00 2001 From: Evie Gauthier Date: Sun, 8 Mar 2026 13:54:41 -0400 Subject: [PATCH 1/6] feat: in-app bug report and feature request modal - Add BugReportModal component with type selector (bug/feature) - Debounced GitHub Search API integration for similar open issues - Auto-populate platform info (Sable version, user agent) for bugs - /report slash command opens the modal - 'Report an Issue' button in Settings > About - Uses existing Jotai atom + hooks pattern (bugReportOpenAtom) - No authentication required; submits to GitHub pre-fill URL --- .changeset/feat_in_app_bug_report.md | 7 + .../features/bug-report/BugReportModal.tsx | 315 ++++++++++++++++++ src/app/features/bug-report/index.ts | 1 + src/app/features/settings/about/About.tsx | 18 + src/app/hooks/useCommands.ts | 15 +- src/app/pages/Router.tsx | 2 + src/app/state/bugReportModal.ts | 3 + src/app/state/hooks/bugReportModal.ts | 15 + 8 files changed, 375 insertions(+), 1 deletion(-) create mode 100644 .changeset/feat_in_app_bug_report.md create mode 100644 src/app/features/bug-report/BugReportModal.tsx create mode 100644 src/app/features/bug-report/index.ts create mode 100644 src/app/state/bugReportModal.ts create mode 100644 src/app/state/hooks/bugReportModal.ts diff --git a/.changeset/feat_in_app_bug_report.md b/.changeset/feat_in_app_bug_report.md new file mode 100644 index 000000000..443b1bec0 --- /dev/null +++ b/.changeset/feat_in_app_bug_report.md @@ -0,0 +1,7 @@ +--- +sable: minor +--- + +feat: in-app bug report and feature request modal + +Adds a `/report` slash command and a "Report an Issue" button on the About settings page. Both open a modal with a type selector (Bug Report / Feature Request), a title field with debounced GitHub issue search for duplicate detection, description and type-specific fields, and auto-populated platform/version info. Submitting opens the pre-filled GitHub new issue page in a new tab — no authentication required. diff --git a/src/app/features/bug-report/BugReportModal.tsx b/src/app/features/bug-report/BugReportModal.tsx new file mode 100644 index 000000000..42ecbc930 --- /dev/null +++ b/src/app/features/bug-report/BugReportModal.tsx @@ -0,0 +1,315 @@ +import { useState, useEffect } from 'react'; +import FocusTrap from 'focus-trap-react'; +import { + Box, + Button, + Chip, + config, + Header, + Icon, + IconButton, + Icons, + Input, + Modal, + Overlay, + OverlayBackdrop, + OverlayCenter, + Scroll, + Spinner, + Text, + TextArea, +} from 'folds'; +import { useCloseBugReportModal, useBugReportModalOpen } from '$state/hooks/bugReportModal'; +import { stopPropagation } from '$utils/keyboard'; + +type ReportType = 'bug' | 'feature'; + +type SimilarIssue = { + number: number; + title: string; + html_url: string; +}; + +const GITHUB_REPO = '7w1/sable'; + +async function searchSimilarIssues(query: string, signal: AbortSignal): Promise { + const params = new URLSearchParams({ + q: `${query} repo:${GITHUB_REPO} is:issue is:open`, + per_page: '5', + }); + const res = await fetch(`https://api.github.com/search/issues?${params}`, { signal }); + if (!res.ok) return []; + const data = (await res.json()) as { items?: SimilarIssue[] }; + return data.items ?? []; +} + +function buildGitHubUrl( + type: ReportType, + title: string, + description: string, + steps: string, + solution: string +): string { + const devLabel = IS_RELEASE_TAG ? '' : '-dev'; + const buildLabel = BUILD_HASH ? ` (${BUILD_HASH})` : ''; + const version = `v${APP_VERSION}${devLabel}${buildLabel}`; + + let body: string; + if (type === 'bug') { + const sections: string[] = [ + `**Describe the bug**\n\n${description}`, + steps ? `**Reproduction**\n\n${steps}` : '', + `**Platform and versions**\n\n- Sable: ${version}\n- Browser: ${navigator.userAgent}`, + ]; + body = sections.filter(Boolean).join('\n\n---\n\n'); + } else { + const sections: string[] = [ + `**Describe the problem**\n\n${description}`, + solution ? `**Describe the solution you'd like**\n\n${solution}` : '', + ]; + body = sections.filter(Boolean).join('\n\n---\n\n'); + } + + const params = new URLSearchParams({ title, body }); + if (type === 'bug') params.set('labels', 'bug'); + if (type === 'feature') params.set('labels', 'enhancement'); + return `https://github.com/${GITHUB_REPO}/issues/new?${params}`; +} + +function BugReportModal() { + const close = useCloseBugReportModal(); + const [type, setType] = useState('bug'); + const [title, setTitle] = useState(''); + const [description, setDescription] = useState(''); + const [steps, setSteps] = useState(''); + const [solution, setSolution] = useState(''); + const [similarIssues, setSimilarIssues] = useState([]); + const [searching, setSearching] = useState(false); + + useEffect(() => { + const trimmed = title.trim(); + const controller = new AbortController(); + let cancelled = false; + let timer: ReturnType | undefined; + + if (trimmed.length >= 3) { + timer = setTimeout(async () => { + setSearching(true); + try { + const issues = await searchSimilarIssues(trimmed, controller.signal); + if (!cancelled) setSimilarIssues(issues); + } catch { + // silently ignore network errors / rate limits + } finally { + if (!cancelled) setSearching(false); + } + }, 600); + } else { + setSimilarIssues([]); + setSearching(false); + } + + return () => { + cancelled = true; + if (timer !== undefined) clearTimeout(timer); + controller.abort(); + }; + }, [title]); + + const canSubmit = title.trim().length > 0; + + const handleSubmit = () => { + if (!canSubmit) return; + const url = buildGitHubUrl( + type, + title.trim(), + description.trim(), + steps.trim(), + solution.trim() + ); + window.open(url, '_blank', 'noopener,noreferrer'); + close(); + }; + + return ( + }> + + + + +
+ + Report an Issue + + + + +
+ + + {/* Type */} + + Type + + setType('bug')} + > + Bug Report + + setType('feature')} + > + Feature Request + + + + + {/* Title + duplicate check */} + + Title * + setTitle((e.target as HTMLInputElement).value)} + /> + {searching && ( + + + Searching for similar issues… + + )} + {!searching && similarIssues.length > 0 && ( + + + Similar open issues — please check before submitting: + + {similarIssues.map((issue) => ( + + {'→ '} + + #{issue.number}: {issue.title} + + + ))} + + )} + + + {/* Description */} + + + {type === 'bug' ? 'Describe the bug *' : 'Describe the problem *'} + +