Skip to content

Commit 7bfc2b0

Browse files
committed
report-issue-flow (squashed)
1 parent 6234886 commit 7bfc2b0

11 files changed

Lines changed: 528 additions & 20 deletions

File tree

src/main/debug.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { appendFileSync, writeFileSync } from 'fs'
1+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'fs'
22
import { join } from 'path'
33
import { app } from 'electron'
44

@@ -34,3 +34,16 @@ export function log(category: string, message: string, data?: unknown): void {
3434
export function getLogFilePath(): string {
3535
return getLogPath()
3636
}
37+
38+
export function readRecentDebugLog(maxLines = 200): string {
39+
const path = getLogPath()
40+
if (!existsSync(path)) return ''
41+
try {
42+
const content = readFileSync(path, 'utf-8')
43+
const lines = content.split('\n')
44+
const tail = lines.slice(-Math.max(1, maxLines))
45+
return tail.join('\n').trim()
46+
} catch {
47+
return ''
48+
}
49+
}

src/main/index.ts

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ import { createNewProject, type GitignorePreset } from './repo-create'
4545
import { isWorktreeMerged } from '../shared/state/prs'
4646
import { watchStatusDir } from './hooks'
4747
import { getAgent, type AgentKind } from './agents'
48+
import { HARNESS_REPO_OWNER, HARNESS_REPO_NAME } from '../shared/constants'
49+
import { readRecentDebugLog } from './debug'
4850

4951
function toAgentKind(value: string | undefined): AgentKind {
5052
return value === 'codex' ? 'codex' : 'claude'
@@ -179,9 +181,9 @@ async function refreshHarnessStarState(): Promise<void> {
179181
store.dispatch({ type: 'settings/harnessStarredChanged', payload: null })
180182
return
181183
}
182-
const starred = await isRepoStarred(token, 'frenchie4111', 'harness')
184+
const starred = await isRepoStarred(token, HARNESS_REPO_OWNER, HARNESS_REPO_NAME)
183185
if (starred === false && !config.harnessAutoStarred) {
184-
const result = await starRepo(token, 'frenchie4111', 'harness')
186+
const result = await starRepo(token, HARNESS_REPO_OWNER, HARNESS_REPO_NAME)
185187
if (result.ok) {
186188
config.harnessAutoStarred = true
187189
saveConfig(config)
@@ -1506,8 +1508,8 @@ function registerIpcHandlers(): void {
15061508
const token = getCachedToken()
15071509
if (!token) return { ok: false, error: 'No GitHub token' }
15081510
const result = starred
1509-
? await starRepo(token, 'frenchie4111', 'harness')
1510-
: await unstarRepo(token, 'frenchie4111', 'harness')
1511+
? await starRepo(token, HARNESS_REPO_OWNER, HARNESS_REPO_NAME)
1512+
: await unstarRepo(token, HARNESS_REPO_OWNER, HARNESS_REPO_NAME)
15111513
if (result.ok) {
15121514
store.dispatch({ type: 'settings/harnessStarredChanged', payload: starred })
15131515
}
@@ -1519,6 +1521,10 @@ function registerIpcHandlers(): void {
15191521
return app.getVersion()
15201522
})
15211523

1524+
transport.onRequest('debug:readRecentLog', (maxLines?: number) => {
1525+
return readRecentDebugLog(maxLines)
1526+
})
1527+
15221528
transport.onRequest('updater:checkForUpdates', async () => {
15231529
if (!app.isPackaged) {
15241530
return { ok: false, error: 'Updates are only available in packaged builds' }
@@ -1705,6 +1711,14 @@ function openNewProjectInFocusedWindow(): void {
17051711
transport.sendSignal('menu:newProject')
17061712
}
17071713

1714+
function openReportIssueInFocusedWindow(): void {
1715+
transport.sendSignal('app:openReportIssue')
1716+
}
1717+
1718+
function crashFocusedTabInFocusedWindow(): void {
1719+
transport.sendSignal('app:debugCrashFocusedTab')
1720+
}
1721+
17081722
function buildMenu(): void {
17091723
const template: Electron.MenuItemConstructorOptions[] = [
17101724
{
@@ -1784,6 +1798,16 @@ function buildMenu(): void {
17841798
{
17851799
label: 'Keyboard Shortcuts',
17861800
click: openKeyboardShortcutsInFocusedWindow
1801+
},
1802+
{ type: 'separator' },
1803+
{
1804+
label: 'Report an Issue…',
1805+
click: openReportIssueInFocusedWindow
1806+
},
1807+
{ type: 'separator' },
1808+
{
1809+
label: 'Debug: Crash Focused Tab',
1810+
click: crashFocusedTabInFocusedWindow
17871811
}
17881812
]
17891813
}

src/preload/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ contextBridge.exposeInMainWorld('api', {
212212

213213
// Updater
214214
getVersion: () => req('updater:getVersion'),
215+
readRecentLog: (maxLines?: number) => req('debug:readRecentLog', maxLines),
215216
checkForUpdates: () => req('updater:checkForUpdates'),
216217
quitAndInstall: () => req('updater:quitAndInstall'),
217218

@@ -245,6 +246,8 @@ contextBridge.exposeInMainWorld('api', {
245246
onTogglePerfMonitor: (callback: () => void) => transport.onSignal('app:togglePerfMonitor', () => callback()),
246247
onOpenKeyboardShortcuts: (callback: () => void) => transport.onSignal('app:openKeyboardShortcuts', () => callback()),
247248
onOpenNewProject: (callback: () => void) => transport.onSignal('menu:newProject', () => callback()),
249+
onOpenReportIssue: (callback: () => void) => transport.onSignal('app:openReportIssue', () => callback()),
250+
onDebugCrashFocusedTab: (callback: () => void) => transport.onSignal('app:debugCrashFocusedTab', () => callback()),
248251

249252
// Hooks
250253
acceptHooks: () => req('hooks:accept'),

src/renderer/App.tsx

Lines changed: 59 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ReviewScreen } from './components/ReviewScreen'
2828
import { CommandPalette } from './components/CommandPalette'
2929
import { HotkeyCheatsheet } from './components/HotkeyCheatsheet'
3030
import { NewProjectScreen } from './components/NewProjectScreen'
31+
import { ReportIssueScreen, onOpenReportIssue, type OpenReportIssueDetail } from './components/ReportIssueScreen'
3132
import iconUrl from '../../resources/icon.png'
3233
import { PerfMonitorHUD } from './components/PerfMonitorHUD'
3334
import { focusTerminalById } from './components/XTerminal'
@@ -187,6 +188,8 @@ export default function App(): JSX.Element {
187188
const [showPerfMonitor, setShowPerfMonitor] = useState(false)
188189
const [showHotkeyCheatsheet, setShowHotkeyCheatsheet] = useState(false)
189190
const [showNewProject, setShowNewProject] = useState(false)
191+
const [reportIssueState, setReportIssueState] = useState<OpenReportIssueDetail | null>(null)
192+
const [crashedTabIds, setCrashedTabIds] = useState<ReadonlySet<string>>(() => new Set())
190193
// `theme` and `defaultAgent` are both seeded at init, so we track
191194
// explicit confirmation separately for the onboarding step checkmarks.
192195
const [themeChosen, setThemeChosen] = useState(false)
@@ -271,6 +274,46 @@ const setQuestStep = useCallback((next: QuestStep) => {
271274
return cleanup
272275
}, [])
273276

277+
// Report Issue — triggered from the Help menu, the sidebar, the
278+
// Settings Support section, and the openReportIssueFor() helper (used
279+
// by the error boundary). Closes any open overlay (Settings, hotkey
280+
// cheatsheet) so the full-screen report takes over the center area.
281+
useEffect(() => {
282+
const openReport = (detail: OpenReportIssueDetail): void => {
283+
setShowSettings(false)
284+
setShowHotkeyCheatsheet(false)
285+
setReportIssueState(detail)
286+
}
287+
const cleanupMenu = window.api.onOpenReportIssue(() => openReport({}))
288+
const cleanupBus = onOpenReportIssue((detail) => openReport(detail))
289+
return () => {
290+
cleanupMenu()
291+
cleanupBus()
292+
}
293+
}, [])
294+
295+
// Debug: Crash Focused Tab (Help menu → for testing the ErrorBoundary).
296+
// Finds the active worktree's active pane and flips its active tab into
297+
// a throwing render. The boundary catches it inside the tab.
298+
useEffect(() => {
299+
return window.api.onDebugCrashFocusedTab(() => {
300+
const wtPath = activeWorktreeId
301+
if (!wtPath) return
302+
const tree = panes[wtPath]
303+
if (!tree) return
304+
const leaves = getLeaves(tree)
305+
const paneId = activePaneId[wtPath] ?? leaves[0]?.id
306+
const leaf = leaves.find((l) => l.id === paneId) ?? leaves[0]
307+
const tabId = leaf?.activeTabId
308+
if (!tabId) return
309+
setCrashedTabIds((prev) => {
310+
const next = new Set(prev)
311+
next.add(tabId)
312+
return next
313+
})
314+
})
315+
}, [activeWorktreeId, panes, activePaneId])
316+
274317
// Trigger a full PR refresh in main. Used by the sidebar refresh button
275318
// and after worktree creation/removal.
276319
const fetchAllPRStatuses = useCallback(() => {
@@ -975,7 +1018,7 @@ const setQuestStep = useCallback((next: QuestStep) => {
9751018
if (!paneTree) return null
9761019
const leaves = getLeaves(paneTree)
9771020
if (leaves.length === 0 || !leaves.some((l) => l.tabs.length > 0)) return null
978-
const isVisible = !showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && wt.path === activeWorktreeId && !pendingDeletionByPath[wt.path]
1021+
const isVisible = !showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && reportIssueState === null && wt.path === activeWorktreeId && !pendingDeletionByPath[wt.path]
9791022
return (
9801023
<div
9811024
key={wt.path}
@@ -992,6 +1035,7 @@ const setQuestStep = useCallback((next: QuestStep) => {
9921035
statuses={statuses}
9931036
shellActivity={shellActivity}
9941037
visible={isVisible}
1038+
crashedTabIds={crashedTabIds}
9951039
nameAgentSessions={nameAgentSessions}
9961040
onSelectTab={handleSelectTab}
9971041
onAddTab={handleAddTerminalTab}
@@ -1019,6 +1063,15 @@ const setQuestStep = useCallback((next: QuestStep) => {
10191063
defaultRepoRoot={activeWorktreeId ? worktreeRepoByPath[activeWorktreeId] : undefined}
10201064
/>
10211065
)}
1066+
{reportIssueState !== null && (
1067+
<ReportIssueScreen
1068+
onClose={() => setReportIssueState(null)}
1069+
initialKind={reportIssueState.kind}
1070+
initialTitle={reportIssueState.title}
1071+
initialBody={reportIssueState.body}
1072+
prefilledContext={reportIssueState.context}
1073+
/>
1074+
)}
10221075
{showActivity && (
10231076
<div className="flex-1 min-w-0 flex">
10241077
<Activity
@@ -1080,12 +1133,12 @@ const setQuestStep = useCallback((next: QuestStep) => {
10801133
</div>
10811134
)
10821135
})()}
1083-
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && !activeWorktreeId && worktrees.length > 0 && (
1136+
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && reportIssueState === null && !activeWorktreeId && worktrees.length > 0 && (
10841137
<div className="flex-1 flex items-center justify-center text-dim">
10851138
Select a worktree to begin
10861139
</div>
10871140
)}
1088-
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && isPendingId(activeWorktreeId) && (() => {
1141+
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && reportIssueState === null && isPendingId(activeWorktreeId) && (() => {
10891142
const pending = pendingWorktrees.find((p) => p.id === activeWorktreeId)
10901143
if (!pending) return null
10911144
return (
@@ -1097,7 +1150,7 @@ const setQuestStep = useCallback((next: QuestStep) => {
10971150
/>
10981151
)
10991152
})()}
1100-
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && activeWorktreeId && pendingDeletionByPath[activeWorktreeId] && (
1153+
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && reportIssueState === null && activeWorktreeId && pendingDeletionByPath[activeWorktreeId] && (
11011154
<DeletingWorktreeScreen
11021155
deletion={pendingDeletionByPath[activeWorktreeId]}
11031156
onDismiss={handleDismissPendingDeletion}
@@ -1109,10 +1162,10 @@ const setQuestStep = useCallback((next: QuestStep) => {
11091162
onFinish={() => setQuestStep('done')}
11101163
/>
11111164
{/* Right panel — hidden on the new-worktree screen so the form gets the full width */}
1112-
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && !rightColumnHidden && (
1165+
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && reportIssueState === null && !rightColumnHidden && (
11131166
<ResizeHandle onDelta={handleRightPanelResize} />
11141167
)}
1115-
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && !rightColumnHidden && (
1168+
{!showNewWorktree && !showActivity && !showCleanup && !showCommandCenter && !showReview && reportIssueState === null && !rightColumnHidden && (
11161169
<RightColumn
11171170
width={rightPanelWidth}
11181171
activeWorktreeId={activeWorktreeId}

src/renderer/components/ErrorBoundary.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Component, type ErrorInfo, type ReactNode } from 'react'
2-
import { AlertTriangle, RotateCcw, Copy, Check, RefreshCw } from 'lucide-react'
2+
import { AlertTriangle, RotateCcw, Copy, Check, RefreshCw, MessageSquare } from 'lucide-react'
3+
import { openReportIssueFor } from './ReportIssueScreen'
34

45
interface FallbackRenderProps {
56
error: Error
@@ -81,6 +82,12 @@ export class ErrorBoundary extends Component<Props, State> {
8182
location.reload()
8283
}
8384

85+
handleReport = (): void => {
86+
const { error, info } = this.state
87+
if (!error) return
88+
openReportIssueFor(error, { componentStack: info?.componentStack ?? '' })
89+
}
90+
8491
toggleExpanded = (): void => {
8592
this.setState((s) => ({ expanded: !s.expanded }))
8693
}
@@ -109,6 +116,7 @@ export class ErrorBoundary extends Component<Props, State> {
109116
</div>
110117
<div className="px-4 py-3 flex flex-wrap items-center gap-2">
111118
<button
119+
type="button"
112120
onClick={this.handleReset}
113121
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-panel border border-border text-fg-bright hover:border-border-strong transition-colors cursor-pointer"
114122
>
@@ -117,6 +125,7 @@ export class ErrorBoundary extends Component<Props, State> {
117125
</button>
118126
{this.props.showReload && (
119127
<button
128+
type="button"
120129
onClick={this.handleReload}
121130
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-panel border border-border text-fg-bright hover:border-border-strong transition-colors cursor-pointer"
122131
>
@@ -125,13 +134,23 @@ export class ErrorBoundary extends Component<Props, State> {
125134
</button>
126135
)}
127136
<button
137+
type="button"
128138
onClick={this.handleCopy}
129139
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-panel border border-border text-fg-bright hover:border-border-strong transition-colors cursor-pointer"
130140
>
131141
{copied ? <Check className="w-3.5 h-3.5 text-success" /> : <Copy className="w-3.5 h-3.5" />}
132142
{copied ? 'Copied' : 'Copy error details'}
133143
</button>
134144
<button
145+
type="button"
146+
onClick={this.handleReport}
147+
className="flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium bg-panel border border-border text-fg-bright hover:border-border-strong transition-colors cursor-pointer"
148+
>
149+
<MessageSquare className="w-3.5 h-3.5" />
150+
Report error
151+
</button>
152+
<button
153+
type="button"
135154
onClick={this.toggleExpanded}
136155
className="ml-auto text-xs text-dim hover:text-fg transition-colors cursor-pointer"
137156
>

0 commit comments

Comments
 (0)