Skip to content
This repository was archived by the owner on Nov 17, 2025. It is now read-only.

Commit e9d9490

Browse files
steipeteclaude
andcommitted
feat: add screenshot debug button to Claude instances list
- Add eye icon button in each Claude instance row when debug mode is enabled - Create ClaudeTerminalScreenshotPopover to capture and display terminal screenshots - Use TTYWindowMappingService to find terminal windows by TTY path - Integrate with WindowInfoHelper to get CGWindowID for screen capture - Button shows popover with terminal screenshot and instance details - Support saving screenshots to desktop with descriptive filenames This helps debug window mapping issues by allowing visual inspection of terminal windows associated with Claude instances. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ad2fb24 commit e9d9490

3 files changed

Lines changed: 267 additions & 0 deletions

File tree

CodeLooper.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
26DEF8E209C4E833EE8C2048 /* Ollama in Frameworks */ = {isa = PBXBuildFile; productRef = 0B727DC7A228F234F413942E /* Ollama */; };
4949
2755A41A7569026C8F18778A /* DocumentPathTracker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8934AB7C0DF5445978C46683 /* DocumentPathTracker.swift */; };
5050
2850357F96675166FD40C6D3 /* XcodeBuildConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 044E767DB568D90A9D156B32 /* XcodeBuildConfigView.swift */; };
51+
2A915D5B906401EE5C4AA430 /* HUDMenuWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69016E540D00CB212180D77A /* HUDMenuWindow.swift */; };
5152
2B231ADF6F02176D061DA120 /* OpenAIProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17FA2368F54DA4948C833482 /* OpenAIProvider.swift */; };
5253
2C2E7C35306F874157E596AC /* CommonTestData.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD43FC5F94134B87E7B8228D /* CommonTestData.swift */; };
5354
2E153716DF27FD15981D5417 /* APIKeyManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 02281B1D972E2A403443EB0A /* APIKeyManager.swift */; };
@@ -193,6 +194,7 @@
193194
AE40CB2D386F76F851DF362B /* cursor-hook.js in Resources */ = {isa = PBXBuildFile; fileRef = 987465FE6147F57B5B370533 /* cursor-hook.js */; };
194195
AEE59E343C16B2410F168E00 /* CommandHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45DA6275BB9C155446F51C09 /* CommandHelpers.swift */; };
195196
AF117898DB9DE292A02E63F4 /* Hummingbird in Frameworks */ = {isa = PBXBuildFile; productRef = 9219D0D29ADC67E6FFC41C9E /* Hummingbird */; };
197+
AFBD9CC04A00D186F6F29151 /* ClaudeTerminalScreenshotPopover.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBA0A7B4F2E9CD8CF3FD1FE7 /* ClaudeTerminalScreenshotPopover.swift */; };
196198
B0A0FBA998D6FCCDC67C0FAE /* StatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 45D9E0B64A840097604A7F12 /* StatsView.swift */; };
197199
B0D851F70AC0DF46828207D8 /* LogManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F256179755CF3F87928E07C4 /* LogManager.swift */; };
198200
B1518551AB661422B2630F3D /* DebugSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165123800EC52083254362A0 /* DebugSettingsView.swift */; };
@@ -215,6 +217,7 @@
215217
C38FC68EC5B5ABC820F98C3A /* Defaults in Frameworks */ = {isa = PBXBuildFile; productRef = 5EC9776FF97D257EE8DCC72B /* Defaults */; };
216218
C4DA4EC486CAAD57B8BE618B /* CodeLooperApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60B16BA7518E957E64AF9F8F /* CodeLooperApp.swift */; };
217219
C56C2BDFC2610F2A42F05006 /* RuleExecutionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0326A9F74898F5007E15ED29 /* RuleExecutionTests.swift */; };
220+
C6D998B235103AF448BE5A80 /* NativePopoverController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5106D3073FAE7B4DC45FE4CF /* NativePopoverController.swift */; };
218221
C7150D5BC7B0B12C4349D194 /* CursorInstanceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5DEDCE2213643BD9D3AD5C4B /* CursorInstanceModels.swift */; };
219222
C9022C479CF0F7AC9C3382FF /* MacOSAutomatorConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E5690C84AB82C7DB84BD6DCA /* MacOSAutomatorConfigView.swift */; };
220223
C964EA15D3C0F430C00B4EB1 /* ThreadSafeBox.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7EBAB588F0605B4C5CA0304 /* ThreadSafeBox.swift */; };
@@ -408,6 +411,7 @@
408411
4D0E00D428925064E6EFD24B /* AIWindowAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AIWindowAnalyzer.swift; sourceTree = "<group>"; };
409412
4D742DBB2C6E98D0F0FF241B /* CursorInputWatcherViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CursorInputWatcherViewModel.swift; sourceTree = "<group>"; };
410413
50A25A5FB23DB0E34CBBC220 /* MainInputFieldHeuristic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainInputFieldHeuristic.swift; sourceTree = "<group>"; };
414+
5106D3073FAE7B4DC45FE4CF /* NativePopoverController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativePopoverController.swift; sourceTree = "<group>"; };
411415
5174C00218163417DA60504F /* AsyncPatterns.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AsyncPatterns.swift; sourceTree = "<group>"; };
412416
521C37D802EABF317FFA3944 /* AppIconStateController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconStateController.swift; sourceTree = "<group>"; };
413417
5223F335B0AAADC0E755A7DC /* ConnectionErrorIndicatorHeuristic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConnectionErrorIndicatorHeuristic.swift; sourceTree = "<group>"; };
@@ -437,6 +441,7 @@
437441
66D291C6C46A13EA5ABFFE32 /* AccessibilityPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessibilityPermissions.swift; sourceTree = "<group>"; };
438442
684ADFED0C0657FCEEA078BA /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = "<group>"; };
439443
68599CD24BE62C1DDDBBE786 /* ollama-swift */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "ollama-swift"; path = "../ollama-swift"; sourceTree = "<group>"; };
444+
69016E540D00CB212180D77A /* HUDMenuWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HUDMenuWindow.swift; sourceTree = "<group>"; };
440445
6943FF4E98060C242A14D838 /* ScreenshotAnalyzer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotAnalyzer.swift; sourceTree = "<group>"; };
441446
6955584328FDBA7FA95B2D9A /* Task+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Task+Timeout.swift"; sourceTree = "<group>"; };
442447
69A663588B1C6DE7B23F48C5 /* MainPopoverView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainPopoverView.swift; sourceTree = "<group>"; };
@@ -568,6 +573,7 @@
568573
E9C56058467A580C6FAFC033 /* DiagnosticsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiagnosticsTests.swift; sourceTree = "<group>"; };
569574
E9D1442BBCD2FA1805B7745A /* ErrorHandlingUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandlingUtility.swift; sourceTree = "<group>"; };
570575
EA71CF8FCCFDA0FB0072AE01 /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = "<group>"; };
576+
EBA0A7B4F2E9CD8CF3FD1FE7 /* ClaudeTerminalScreenshotPopover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClaudeTerminalScreenshotPopover.swift; sourceTree = "<group>"; };
571577
EE3BB643B3D258F00DFA6762 /* InterventionEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InterventionEngineTests.swift; sourceTree = "<group>"; };
572578
EFFE475498AF35EF793EFE0C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
573579
F0B9729B09602019CB5866EF /* LogCategory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LogCategory.swift; sourceTree = "<group>"; };
@@ -708,9 +714,12 @@
708714
children = (
709715
521C37D802EABF317FFA3944 /* AppIconStateController.swift */,
710716
9E7B41C8E7FEA8B47BB303DB /* ClaudeInstancesList.swift */,
717+
EBA0A7B4F2E9CD8CF3FD1FE7 /* ClaudeTerminalScreenshotPopover.swift */,
711718
CD48A8F36F6F295C5068D703 /* CustomMenuWindow.swift */,
719+
69016E540D00CB212180D77A /* HUDMenuWindow.swift */,
712720
69A663588B1C6DE7B23F48C5 /* MainPopoverView.swift */,
713721
B6BAE97B9CA85115B2ED3031 /* MenuBarStatusView.swift */,
722+
5106D3073FAE7B4DC45FE4CF /* NativePopoverController.swift */,
714723
456D6E1466D850D7B4432DF1 /* StatusBarController.swift */,
715724
C7F87E01850EB3AD8908A5A6 /* StatusIndicators.swift */,
716725
);
@@ -1867,9 +1876,12 @@
18671876
E3AFE5ECE90642DFC8E72043 /* UploadCompletionFeedback.swift in Sources */,
18681877
85F11293F5276E948EF17DF9 /* AppIconStateController.swift in Sources */,
18691878
4AA21744D490EA8A2CDEBA0B /* ClaudeInstancesList.swift in Sources */,
1879+
AFBD9CC04A00D186F6F29151 /* ClaudeTerminalScreenshotPopover.swift in Sources */,
18701880
DA6F200CAC58C9CE5C5C50FD /* CustomMenuWindow.swift in Sources */,
1881+
2A915D5B906401EE5C4AA430 /* HUDMenuWindow.swift in Sources */,
18711882
D6506A1CBB22DB21B67E0E2D /* MainPopoverView.swift in Sources */,
18721883
7791A26DE75B43DAED4284F2 /* MenuBarStatusView.swift in Sources */,
1884+
C6D998B235103AF448BE5A80 /* NativePopoverController.swift in Sources */,
18731885
4AF986F21576D42B1D0DCA65 /* StatusBarController.swift in Sources */,
18741886
A6552E5E86BF245B84DE09CC /* StatusIndicators.swift in Sources */,
18751887
);

Features/StatusBar/UI/ClaudeInstancesList.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,22 @@ private struct ClaudeInstanceRow: View {
5454

5555
Spacer()
5656

57+
// Screenshot button when debug mode is enabled
58+
if showDebugInfo {
59+
Button(action: {
60+
showScreenshotPopover = true
61+
}, label: {
62+
Image(systemName: "eye")
63+
.font(.system(size: 11))
64+
.foregroundColor(ColorPalette.loopTint)
65+
})
66+
.buttonStyle(.plain)
67+
.help("View Terminal Screenshot")
68+
.popover(isPresented: $showScreenshotPopover) {
69+
ClaudeTerminalScreenshotPopover(instance: instance)
70+
}
71+
}
72+
5773
// PID and TTY in compact form
5874
if showDebugInfo {
5975
Text("PID: \(instance.pid)")
@@ -113,6 +129,7 @@ private struct ClaudeInstanceRow: View {
113129

114130
private static let logger = Logger(category: .ui)
115131
@State private var isHovering = false
132+
@State private var showScreenshotPopover = false
116133

117134
// MARK: - Helper Functions
118135

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
import AppKit
2+
import AXorcist
3+
import DesignSystem
4+
import Diagnostics
5+
@preconcurrency import ScreenCaptureKit
6+
import SwiftUI
7+
8+
struct ClaudeTerminalScreenshotPopover: View {
9+
let instance: ClaudeInstance
10+
11+
@State private var screenshotImage: NSImage?
12+
@State private var isLoading = false
13+
@State private var errorMessage: String?
14+
@StateObject private var screenshotAnalyzer = CursorScreenshotAnalyzer()
15+
16+
private let logger = Logger(category: .ui)
17+
18+
var body: some View {
19+
VStack(spacing: Spacing.medium) {
20+
// Header
21+
HStack {
22+
Image(systemName: "terminal")
23+
.foregroundColor(ColorPalette.accent)
24+
Text("Terminal Screenshot")
25+
.font(Typography.callout(.semibold))
26+
Spacer()
27+
}
28+
29+
// Screenshot content
30+
Group {
31+
if isLoading {
32+
VStack(spacing: Spacing.small) {
33+
ProgressView()
34+
.progressViewStyle(CircularProgressViewStyle())
35+
Text("Capturing terminal window...")
36+
.font(Typography.caption1())
37+
.foregroundColor(ColorPalette.textSecondary)
38+
}
39+
.frame(width: 400, height: 300)
40+
} else if let image = screenshotImage {
41+
Image(nsImage: image)
42+
.resizable()
43+
.aspectRatio(contentMode: .fit)
44+
.frame(maxWidth: 600, maxHeight: 400)
45+
.cornerRadius(8)
46+
.shadow(radius: 2)
47+
} else if let error = errorMessage {
48+
VStack(spacing: Spacing.small) {
49+
Image(systemName: "exclamationmark.triangle")
50+
.font(.largeTitle)
51+
.foregroundColor(ColorPalette.warning)
52+
Text("Failed to capture screenshot")
53+
.font(Typography.callout(.medium))
54+
Text(error)
55+
.font(Typography.caption1())
56+
.foregroundColor(ColorPalette.textSecondary)
57+
.multilineTextAlignment(.center)
58+
}
59+
.frame(width: 400, height: 300)
60+
} else {
61+
VStack(spacing: Spacing.small) {
62+
Image(systemName: "photo")
63+
.font(.largeTitle)
64+
.foregroundColor(ColorPalette.textSecondary)
65+
Text("No screenshot available")
66+
.font(Typography.callout(.medium))
67+
.foregroundColor(ColorPalette.textSecondary)
68+
}
69+
.frame(width: 400, height: 300)
70+
}
71+
}
72+
73+
// Instance info
74+
VStack(alignment: .leading, spacing: Spacing.xSmall) {
75+
HStack {
76+
Text("Folder:")
77+
.font(Typography.caption1(.medium))
78+
.foregroundColor(ColorPalette.textSecondary)
79+
Text(instance.folderName)
80+
.font(Typography.caption1())
81+
.foregroundColor(ColorPalette.text)
82+
Spacer()
83+
}
84+
85+
HStack {
86+
Text("TTY:")
87+
.font(Typography.caption1(.medium))
88+
.foregroundColor(ColorPalette.textSecondary)
89+
Text(instance.ttyPath)
90+
.font(Typography.caption2())
91+
.foregroundColor(ColorPalette.textTertiary)
92+
.textSelection(.enabled)
93+
Spacer()
94+
}
95+
96+
HStack {
97+
Text("PID:")
98+
.font(Typography.caption1(.medium))
99+
.foregroundColor(ColorPalette.textSecondary)
100+
Text(String(instance.pid))
101+
.font(Typography.caption2())
102+
.foregroundColor(ColorPalette.textTertiary)
103+
Spacer()
104+
}
105+
}
106+
.padding(.horizontal, Spacing.small)
107+
.padding(.vertical, Spacing.xSmall)
108+
.background(ColorPalette.backgroundTertiary)
109+
.cornerRadius(6)
110+
111+
// Action buttons
112+
HStack(spacing: Spacing.medium) {
113+
DSButton("Refresh", style: .secondary, size: .small) {
114+
captureScreenshot()
115+
}
116+
.disabled(isLoading)
117+
118+
if screenshotImage != nil {
119+
DSButton("Save to Desktop", style: .primary, size: .small) {
120+
saveImageToDesktop()
121+
}
122+
}
123+
}
124+
}
125+
.padding(Spacing.large)
126+
.frame(minWidth: 500, maxWidth: 700)
127+
.withDesignSystem()
128+
.onAppear {
129+
captureScreenshot()
130+
}
131+
}
132+
133+
private func captureScreenshot() {
134+
isLoading = true
135+
errorMessage = nil
136+
screenshotImage = nil
137+
138+
Task {
139+
do {
140+
// First, find the terminal window using TTY mapping
141+
guard let terminalWindow = await findTerminalWindow() else {
142+
await MainActor.run {
143+
self.isLoading = false
144+
self.errorMessage = "Could not find terminal window for this Claude instance"
145+
}
146+
return
147+
}
148+
149+
// Find the SCWindow for this terminal window
150+
let scWindow = await findSCWindow(for: terminalWindow)
151+
152+
// Capture the screenshot
153+
let image = try await screenshotAnalyzer.captureCursorWindow(targetSCWindow: scWindow)
154+
155+
await MainActor.run {
156+
self.screenshotImage = image
157+
self.isLoading = false
158+
159+
if image == nil {
160+
self.errorMessage = "Could not capture window. The window may be minimized or hidden."
161+
}
162+
}
163+
} catch {
164+
await MainActor.run {
165+
self.isLoading = false
166+
self.errorMessage = error.localizedDescription
167+
logger.error("Failed to capture screenshot for Claude instance \(instance.id): \(error)")
168+
}
169+
}
170+
}
171+
}
172+
173+
@MainActor
174+
private func findTerminalWindow() async -> Element? {
175+
// Use TTYWindowMappingService to find the window
176+
if !instance.ttyPath.isEmpty {
177+
return TTYWindowMappingService.shared.findWindowForTTY(instance.ttyPath)
178+
}
179+
return nil
180+
}
181+
182+
private func findSCWindow(for element: Element) async -> SCWindow? {
183+
do {
184+
let content = try await SCShareableContent.excludingDesktopWindows(false, onScreenWindowsOnly: true)
185+
186+
// Get window ID from the element using WindowInfoHelper
187+
guard let windowID = WindowInfoHelper.getWindowID(from: element) else {
188+
logger.warning("Could not get window ID from element")
189+
return nil
190+
}
191+
192+
// Find matching SCWindow
193+
return content.windows.first { window in
194+
window.windowID == windowID
195+
}
196+
} catch {
197+
logger.error("Failed to get shareable content: \(error)")
198+
return nil
199+
}
200+
}
201+
202+
private func saveImageToDesktop() {
203+
guard let image = screenshotImage else { return }
204+
205+
let desktopURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first!
206+
let timestamp = DateFormatter().apply { formatter in
207+
formatter.dateFormat = "yyyy-MM-dd_HH-mm-ss"
208+
}.string(from: Date())
209+
let filename = "CodeLooper_Terminal_\(instance.folderName)_\(timestamp).png"
210+
let fileURL = desktopURL.appendingPathComponent(filename)
211+
212+
if let tiffData = image.tiffRepresentation,
213+
let bitmapRep = NSBitmapImageRep(data: tiffData),
214+
let pngData = bitmapRep.representation(using: .png, properties: [:]) {
215+
216+
do {
217+
try pngData.write(to: fileURL)
218+
logger.info("Screenshot saved to: \(fileURL.path)")
219+
220+
// Show in Finder
221+
NSWorkspace.shared.activateFileViewerSelecting([fileURL])
222+
} catch {
223+
logger.error("Failed to save screenshot: \(error)")
224+
self.errorMessage = "Failed to save screenshot: \(error.localizedDescription)"
225+
}
226+
}
227+
}
228+
}
229+
230+
// MARK: - Extension for DateFormatter
231+
232+
private extension DateFormatter {
233+
func apply(_ closure: (DateFormatter) -> Void) -> DateFormatter {
234+
closure(self)
235+
return self
236+
}
237+
}
238+

0 commit comments

Comments
 (0)