create-screenfix 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/bin/create-screenfix.js +40 -0
  2. package/lib/doctor.js +120 -0
  3. package/lib/init.js +165 -0
  4. package/lib/uninstall.js +67 -0
  5. package/lib/update.js +62 -0
  6. package/package.json +35 -0
  7. package/python/build/lib/screenfix/__init__.py +3 -0
  8. package/python/build/lib/screenfix/annotation_window.py +222 -0
  9. package/python/build/lib/screenfix/clipboard_watcher.py +164 -0
  10. package/python/build/lib/screenfix/config.py +66 -0
  11. package/python/build/lib/screenfix/daemon.py +204 -0
  12. package/python/build/lib/screenfix/mcp_server.py +318 -0
  13. package/python/build/lib/screenfix/task_tracker.py +129 -0
  14. package/python/pyproject.toml +24 -0
  15. package/python/requirements.txt +5 -0
  16. package/python/screenfix/__init__.py +3 -0
  17. package/python/screenfix/annotation_window.py +222 -0
  18. package/python/screenfix/clipboard_watcher.py +164 -0
  19. package/python/screenfix/config.py +66 -0
  20. package/python/screenfix/daemon.py +204 -0
  21. package/python/screenfix/mcp_server.py +318 -0
  22. package/python/screenfix/task_tracker.py +129 -0
  23. package/python/screenfix.egg-info/PKG-INFO +11 -0
  24. package/python/screenfix.egg-info/SOURCES.txt +14 -0
  25. package/python/screenfix.egg-info/dependency_links.txt +1 -0
  26. package/python/screenfix.egg-info/entry_points.txt +3 -0
  27. package/python/screenfix.egg-info/requires.txt +5 -0
  28. package/python/screenfix.egg-info/top_level.txt +1 -0
  29. package/templates/commands/screenfix-list-tasks.md +6 -0
  30. package/templates/commands/screenfix-start.md +8 -0
  31. package/templates/commands/screenfix-status.md +6 -0
  32. package/templates/commands/screenfix-stop.md +6 -0
  33. package/templates/commands/screenfix-tasks-next.md +14 -0
  34. package/templates/commands/screenfix-tasks-yolo.md +14 -0
@@ -0,0 +1,318 @@
1
+ """MCP server for ScreenFix."""
2
+
3
+ import asyncio
4
+ import base64
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import time
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ from mcp.server import Server
14
+ from mcp.server.stdio import stdio_server
15
+ from mcp.types import Tool, TextContent, ImageContent
16
+
17
+ from .config import config
18
+ from .task_tracker import get_tasks, get_pending_tasks, mark_task_complete
19
+
20
+
21
+ STATE_FILE = Path.home() / ".config" / "screenfix" / "state.json"
22
+
23
+
24
+ def get_daemon_state() -> dict:
25
+ """Read the daemon state file."""
26
+ if not STATE_FILE.exists():
27
+ return {"running": False, "listening": False}
28
+
29
+ try:
30
+ with open(STATE_FILE, "r") as f:
31
+ state = json.load(f)
32
+
33
+ pid = state.get("pid")
34
+ if pid:
35
+ try:
36
+ os.kill(pid, 0)
37
+ state["running"] = True
38
+ except OSError:
39
+ state["running"] = False
40
+ else:
41
+ state["running"] = False
42
+
43
+ return state
44
+ except (json.JSONDecodeError, IOError):
45
+ return {"running": False, "listening": False}
46
+
47
+
48
+ def get_screenshots() -> list[dict]:
49
+ """Get list of screenshots."""
50
+ save_dir = Path(config.save_directory)
51
+ if not save_dir.exists():
52
+ return []
53
+
54
+ screenshots = []
55
+ for f in save_dir.glob("*.png"):
56
+ screenshots.append({
57
+ "path": str(f),
58
+ "filename": f.name,
59
+ "modified": f.stat().st_mtime,
60
+ })
61
+
62
+ screenshots.sort(key=lambda x: x["modified"], reverse=True)
63
+ return screenshots
64
+
65
+
66
+ def get_last_screenshot() -> dict | None:
67
+ """Get the most recent screenshot."""
68
+ screenshots = get_screenshots()
69
+ return screenshots[0] if screenshots else None
70
+
71
+
72
+ def start_daemon() -> tuple[bool, str]:
73
+ """Start the daemon as a background process."""
74
+ state = get_daemon_state()
75
+ if state.get("running"):
76
+ return True, "Daemon is already running"
77
+
78
+ try:
79
+ log_dir = Path.home() / ".config" / "screenfix"
80
+ log_dir.mkdir(parents=True, exist_ok=True)
81
+ log_file = log_dir / "daemon.log"
82
+
83
+ with open(log_file, "a") as log:
84
+ subprocess.Popen(
85
+ [sys.executable, "-m", "screenfix.daemon"],
86
+ cwd=os.getcwd(),
87
+ stdout=log,
88
+ stderr=log,
89
+ start_new_session=True,
90
+ )
91
+
92
+ time.sleep(0.5)
93
+
94
+ new_state = get_daemon_state()
95
+ if new_state.get("running"):
96
+ return True, f"Daemon started (PID: {new_state.get('pid')}). Use Cmd+Ctrl+Shift+4 to capture."
97
+ else:
98
+ return True, f"Daemon starting... Check logs at {log_file}"
99
+
100
+ except Exception as e:
101
+ return False, f"Failed to start daemon: {e}"
102
+
103
+
104
+ def stop_daemon() -> tuple[bool, str]:
105
+ """Stop the daemon."""
106
+ state = get_daemon_state()
107
+ if not state.get("running"):
108
+ return True, "Daemon is not running"
109
+
110
+ pid = state.get("pid")
111
+ if pid:
112
+ try:
113
+ os.kill(pid, 15)
114
+ return True, "Daemon stopped"
115
+ except OSError as e:
116
+ return False, f"Failed to stop daemon: {e}"
117
+
118
+ return False, "Could not find daemon PID"
119
+
120
+
121
+ def create_server() -> Server:
122
+ """Create the MCP server."""
123
+ server = Server("screenfix")
124
+
125
+ @server.list_tools()
126
+ async def list_tools() -> list[Tool]:
127
+ return [
128
+ Tool(
129
+ name="start_daemon",
130
+ description="Start ScreenFix daemon. Use Cmd+Ctrl+Shift+4 to capture screenshots instantly.",
131
+ inputSchema={"type": "object", "properties": {}, "required": []},
132
+ ),
133
+ Tool(
134
+ name="stop_daemon",
135
+ description="Stop the ScreenFix daemon",
136
+ inputSchema={"type": "object", "properties": {}, "required": []},
137
+ ),
138
+ Tool(
139
+ name="get_status",
140
+ description="Get ScreenFix status",
141
+ inputSchema={"type": "object", "properties": {}, "required": []},
142
+ ),
143
+ Tool(
144
+ name="get_last_screenshot",
145
+ description="Get the most recent screenshot with its image and related task",
146
+ inputSchema={
147
+ "type": "object",
148
+ "properties": {
149
+ "include_image": {
150
+ "type": "boolean",
151
+ "description": "Include image data",
152
+ "default": True,
153
+ }
154
+ },
155
+ "required": [],
156
+ },
157
+ ),
158
+ Tool(
159
+ name="list_screenshots",
160
+ description="List all screenshots",
161
+ inputSchema={
162
+ "type": "object",
163
+ "properties": {
164
+ "limit": {"type": "integer", "default": 10}
165
+ },
166
+ "required": [],
167
+ },
168
+ ),
169
+ Tool(
170
+ name="get_tasks",
171
+ description="Get tasks from tasks.md",
172
+ inputSchema={
173
+ "type": "object",
174
+ "properties": {
175
+ "pending_only": {"type": "boolean", "default": False}
176
+ },
177
+ "required": [],
178
+ },
179
+ ),
180
+ Tool(
181
+ name="complete_task",
182
+ description="Mark a task as complete",
183
+ inputSchema={
184
+ "type": "object",
185
+ "properties": {
186
+ "task_text": {"type": "string", "description": "Task text to mark complete"}
187
+ },
188
+ "required": ["task_text"],
189
+ },
190
+ ),
191
+ Tool(
192
+ name="read_screenshot",
193
+ description="Read a specific screenshot by path",
194
+ inputSchema={
195
+ "type": "object",
196
+ "properties": {
197
+ "path": {"type": "string", "description": "Path to screenshot"}
198
+ },
199
+ "required": ["path"],
200
+ },
201
+ ),
202
+ ]
203
+
204
+ @server.call_tool()
205
+ async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent | ImageContent]:
206
+
207
+ if name == "start_daemon":
208
+ success, message = start_daemon()
209
+ return [TextContent(type="text", text=message)]
210
+
211
+ elif name == "stop_daemon":
212
+ success, message = stop_daemon()
213
+ return [TextContent(type="text", text=message)]
214
+
215
+ elif name == "get_status":
216
+ state = get_daemon_state()
217
+ screenshots = get_screenshots()
218
+ tasks = get_pending_tasks()
219
+
220
+ status = "Running" if state.get("running") else "Not running"
221
+ text = f"""ScreenFix Status:
222
+ - Daemon: {status}
223
+ - Screenshots: {len(screenshots)}
224
+ - Pending tasks: {len(tasks)}
225
+
226
+ Use Cmd+Ctrl+Shift+4 to capture (instant, no delay)"""
227
+ return [TextContent(type="text", text=text)]
228
+
229
+ elif name == "get_last_screenshot":
230
+ include_image = arguments.get("include_image", True)
231
+ screenshot = get_last_screenshot()
232
+
233
+ if not screenshot:
234
+ return [TextContent(type="text", text="No screenshots found. Use Cmd+Ctrl+Shift+4 to capture.")]
235
+
236
+ result = [TextContent(type="text", text=f"Screenshot: {screenshot['path']}")]
237
+
238
+ if include_image and os.path.exists(screenshot["path"]):
239
+ with open(screenshot["path"], "rb") as f:
240
+ image_data = base64.standard_b64encode(f.read()).decode("utf-8")
241
+ result.append(ImageContent(type="image", data=image_data, mimeType="image/png"))
242
+
243
+ tasks = get_tasks()
244
+ for task in tasks:
245
+ if task.get("screenshot") == screenshot["path"]:
246
+ result.append(TextContent(
247
+ type="text",
248
+ text=f"\nTask: {task['text']}\nStatus: {'Done' if task['completed'] else 'Pending'}"
249
+ ))
250
+ break
251
+
252
+ return result
253
+
254
+ elif name == "list_screenshots":
255
+ limit = arguments.get("limit", 10)
256
+ screenshots = get_screenshots()[:limit]
257
+
258
+ if not screenshots:
259
+ return [TextContent(type="text", text="No screenshots found.")]
260
+
261
+ lines = ["Screenshots:"]
262
+ for i, s in enumerate(screenshots, 1):
263
+ lines.append(f"{i}. {s['filename']}")
264
+
265
+ return [TextContent(type="text", text="\n".join(lines))]
266
+
267
+ elif name == "get_tasks":
268
+ pending_only = arguments.get("pending_only", False)
269
+ tasks = get_pending_tasks() if pending_only else get_tasks()
270
+
271
+ if not tasks:
272
+ return [TextContent(type="text", text="No tasks found.")]
273
+
274
+ lines = ["Tasks:"]
275
+ for i, task in enumerate(tasks, 1):
276
+ status = "[x]" if task["completed"] else "[ ]"
277
+ lines.append(f"{i}. {status} {task['text']}")
278
+
279
+ return [TextContent(type="text", text="\n".join(lines))]
280
+
281
+ elif name == "complete_task":
282
+ task_text = arguments.get("task_text", "")
283
+ if mark_task_complete(task_text):
284
+ return [TextContent(type="text", text=f"Completed: {task_text}")]
285
+ return [TextContent(type="text", text=f"Task not found: {task_text}")]
286
+
287
+ elif name == "read_screenshot":
288
+ path = arguments.get("path", "")
289
+ if not path or not os.path.exists(path):
290
+ return [TextContent(type="text", text=f"Not found: {path}")]
291
+
292
+ with open(path, "rb") as f:
293
+ image_data = base64.standard_b64encode(f.read()).decode("utf-8")
294
+
295
+ return [
296
+ TextContent(type="text", text=f"Screenshot: {path}"),
297
+ ImageContent(type="image", data=image_data, mimeType="image/png"),
298
+ ]
299
+
300
+ return [TextContent(type="text", text=f"Unknown tool: {name}")]
301
+
302
+ return server
303
+
304
+
305
+ async def run_server():
306
+ """Run the MCP server."""
307
+ server = create_server()
308
+ async with stdio_server() as (read_stream, write_stream):
309
+ await server.run(read_stream, write_stream, server.create_initialization_options())
310
+
311
+
312
+ def main():
313
+ """Main entry point."""
314
+ asyncio.run(run_server())
315
+
316
+
317
+ if __name__ == "__main__":
318
+ main()
@@ -0,0 +1,129 @@
1
+ """Task tracker for managing tasks.md file."""
2
+
3
+ import os
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Optional
7
+ from .config import config
8
+
9
+
10
+ def add_task(instruction: str, screenshot_path: Optional[str] = None) -> str:
11
+ """
12
+ Add a task to the tasks.md file.
13
+
14
+ Args:
15
+ instruction: Task instruction text
16
+ screenshot_path: Optional path to related screenshot
17
+
18
+ Returns:
19
+ The formatted task entry that was added
20
+ """
21
+ tasks_path = Path(config.tasks_file)
22
+
23
+ # Create parent directory if needed
24
+ tasks_path.parent.mkdir(parents=True, exist_ok=True)
25
+
26
+ # Create file with header if it doesn't exist
27
+ if not tasks_path.exists():
28
+ with open(tasks_path, "w") as f:
29
+ f.write("# Tasks for Claude Code\n\n")
30
+
31
+ # Format the task entry
32
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M")
33
+
34
+ # Build task lines
35
+ task_lines = [f"- [ ] {instruction.strip()}"]
36
+
37
+ # Add screenshot reference if provided
38
+ if screenshot_path:
39
+ task_lines.append(f" - Screenshot: `{screenshot_path}`")
40
+
41
+ task_lines.append(f" - Added: {timestamp}")
42
+ task_lines.append("") # Blank line after entry
43
+
44
+ task_entry = "\n".join(task_lines) + "\n"
45
+
46
+ # Append to file
47
+ with open(tasks_path, "a") as f:
48
+ f.write(task_entry)
49
+
50
+ return task_entry
51
+
52
+
53
+ def get_tasks() -> list[dict]:
54
+ """
55
+ Read all tasks from tasks.md.
56
+
57
+ Returns:
58
+ List of task dictionaries with keys: text, completed, screenshot, added
59
+ """
60
+ tasks_path = Path(config.tasks_file)
61
+
62
+ if not tasks_path.exists():
63
+ return []
64
+
65
+ tasks = []
66
+ current_task = None
67
+
68
+ with open(tasks_path, "r") as f:
69
+ for line in f:
70
+ # Check for task line
71
+ if line.startswith("- [ ] ") or line.startswith("- [x] "):
72
+ if current_task:
73
+ tasks.append(current_task)
74
+ completed = line.startswith("- [x] ")
75
+ current_task = {
76
+ "text": line[6:].strip(),
77
+ "completed": completed,
78
+ "screenshot": None,
79
+ "added": None,
80
+ }
81
+ elif current_task and line.strip().startswith("- Screenshot:"):
82
+ # Extract screenshot path
83
+ path = line.strip()[14:].strip().strip("`")
84
+ current_task["screenshot"] = path
85
+ elif current_task and line.strip().startswith("- Added:"):
86
+ # Extract timestamp
87
+ current_task["added"] = line.strip()[9:].strip()
88
+
89
+ # Don't forget the last task
90
+ if current_task:
91
+ tasks.append(current_task)
92
+
93
+ return tasks
94
+
95
+
96
+ def get_pending_tasks() -> list[dict]:
97
+ """Get only incomplete tasks."""
98
+ return [t for t in get_tasks() if not t["completed"]]
99
+
100
+
101
+ def mark_task_complete(task_text: str) -> bool:
102
+ """
103
+ Mark a task as complete by its text.
104
+
105
+ Args:
106
+ task_text: The task text to find and mark complete
107
+
108
+ Returns:
109
+ True if task was found and marked, False otherwise
110
+ """
111
+ tasks_path = Path(config.tasks_file)
112
+
113
+ if not tasks_path.exists():
114
+ return False
115
+
116
+ with open(tasks_path, "r") as f:
117
+ content = f.read()
118
+
119
+ # Find and replace the checkbox
120
+ search = f"- [ ] {task_text}"
121
+ replace = f"- [x] {task_text}"
122
+
123
+ if search in content:
124
+ content = content.replace(search, replace, 1)
125
+ with open(tasks_path, "w") as f:
126
+ f.write(content)
127
+ return True
128
+
129
+ return False
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "screenfix"
7
+ version = "0.1.0"
8
+ description = "Screenshot capture tool for Claude Code via MCP"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "mcp>=1.0.0",
13
+ "pyobjc-core>=10.0",
14
+ "pyobjc-framework-Cocoa>=10.0",
15
+ "pyobjc-framework-Quartz>=10.0",
16
+ "Pillow>=10.0.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ screenfix-daemon = "screenfix.daemon:main"
21
+ screenfix-mcp = "screenfix.mcp_server:main"
22
+
23
+ [tool.setuptools.packages.find]
24
+ where = ["."]
@@ -0,0 +1,5 @@
1
+ mcp>=1.0.0
2
+ pyobjc-core>=10.0
3
+ pyobjc-framework-Cocoa>=10.0
4
+ pyobjc-framework-Quartz>=10.0
5
+ Pillow>=10.0.0
@@ -0,0 +1,3 @@
1
+ """ScreenFix - Screen snipping tool integrated with Claude Code via MCP."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,222 @@
1
+ """Annotation window for adding instructions to screenshots."""
2
+
3
+ import os
4
+ import shutil
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from AppKit import (
9
+ NSWindow,
10
+ NSWindowStyleMaskTitled,
11
+ NSWindowStyleMaskClosable,
12
+ NSWindowStyleMaskNonactivatingPanel,
13
+ NSBackingStoreBuffered,
14
+ NSImageView,
15
+ NSImage,
16
+ NSScrollView,
17
+ NSTextView,
18
+ NSButton,
19
+ NSBezelStyleRounded,
20
+ NSTextField,
21
+ NSFont,
22
+ NSApplication,
23
+ NSImageScaleProportionallyDown,
24
+ NSPanel,
25
+ NSWindowCollectionBehaviorCanJoinAllSpaces,
26
+ NSWindowCollectionBehaviorFullScreenAuxiliary,
27
+ )
28
+ from Quartz import kCGMaximumWindowLevelKey, CGWindowLevelForKey
29
+ from Foundation import NSMakeRect, NSObject
30
+ import objc
31
+
32
+ from .config import config
33
+ from .task_tracker import add_task
34
+
35
+
36
+ _current_controller = None
37
+
38
+
39
+ def save_screenshot(temp_path: str) -> str:
40
+ """Save screenshot from temp location to final location."""
41
+ save_dir = Path(config.save_directory)
42
+ save_dir.mkdir(parents=True, exist_ok=True)
43
+
44
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
45
+ filename = f"screenshot_{timestamp}.png"
46
+ final_path = save_dir / filename
47
+
48
+ shutil.move(temp_path, final_path)
49
+ return str(final_path)
50
+
51
+
52
+ def cleanup_temp_file(temp_path: str):
53
+ """Remove temporary file."""
54
+ try:
55
+ if os.path.exists(temp_path):
56
+ os.remove(temp_path)
57
+ except OSError:
58
+ pass
59
+
60
+
61
+ class AnnotationWindowDelegate(NSObject):
62
+ """Delegate to handle window events."""
63
+
64
+ def initWithController_(self, controller):
65
+ self = objc.super(AnnotationWindowDelegate, self).init()
66
+ if self is None:
67
+ return None
68
+ self.controller = controller
69
+ return self
70
+
71
+ def windowWillClose_(self, notification):
72
+ """Called when window is about to close."""
73
+ global _current_controller
74
+ _current_controller = None
75
+
76
+
77
+ class AnnotationWindowController(NSObject):
78
+ """Controller for the annotation window."""
79
+
80
+ def initWithImagePath_(self, image_path):
81
+ self = objc.super(AnnotationWindowController, self).init()
82
+ if self is None:
83
+ return None
84
+
85
+ self.image_path = image_path
86
+ self.window = None
87
+ self.text_view = None
88
+ self.delegate = None
89
+
90
+ self._create_window()
91
+ return self
92
+
93
+ def _create_window(self):
94
+ """Create the annotation window."""
95
+ image = NSImage.alloc().initWithContentsOfFile_(self.image_path)
96
+ if not image:
97
+ return
98
+
99
+ img_size = image.size()
100
+
101
+ max_display_width = 400
102
+ max_display_height = 300
103
+
104
+ width_scale = min(1.0, max_display_width / img_size.width) if img_size.width > 0 else 1.0
105
+ height_scale = min(1.0, max_display_height / img_size.height) if img_size.height > 0 else 1.0
106
+ scale = min(width_scale, height_scale)
107
+
108
+ display_width = int(img_size.width * scale)
109
+ display_height = int(img_size.height * scale)
110
+
111
+ padding = 20
112
+ window_width = max(display_width + padding * 2, 450)
113
+ text_area_height = 80
114
+ button_area_height = 50
115
+ label_height = 25
116
+ window_height = display_height + text_area_height + button_area_height + label_height + padding * 3
117
+
118
+ style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskNonactivatingPanel
119
+ self.window = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
120
+ NSMakeRect(200, 200, window_width, window_height),
121
+ style,
122
+ NSBackingStoreBuffered,
123
+ False,
124
+ )
125
+ self.window.setTitle_("ScreenFix - Add Instructions")
126
+
127
+ self.window.setFloatingPanel_(True)
128
+ self.window.setHidesOnDeactivate_(False)
129
+
130
+ max_level = CGWindowLevelForKey(kCGMaximumWindowLevelKey)
131
+ self.window.setLevel_(max_level - 1)
132
+
133
+ self.window.setCollectionBehavior_(
134
+ NSWindowCollectionBehaviorCanJoinAllSpaces |
135
+ NSWindowCollectionBehaviorFullScreenAuxiliary
136
+ )
137
+
138
+ self.delegate = AnnotationWindowDelegate.alloc().initWithController_(self)
139
+ self.window.setDelegate_(self.delegate)
140
+
141
+ content = self.window.contentView()
142
+ y_pos = padding
143
+
144
+ # Cancel button
145
+ cancel_btn = NSButton.alloc().initWithFrame_(NSMakeRect(padding, y_pos, 100, 32))
146
+ cancel_btn.setTitle_("Cancel")
147
+ cancel_btn.setBezelStyle_(NSBezelStyleRounded)
148
+ cancel_btn.setTarget_(self)
149
+ cancel_btn.setAction_(objc.selector(self.cancel_, signature=b"v@:@"))
150
+ content.addSubview_(cancel_btn)
151
+
152
+ # Save button
153
+ save_btn = NSButton.alloc().initWithFrame_(NSMakeRect(window_width - padding - 100, y_pos, 100, 32))
154
+ save_btn.setTitle_("Save")
155
+ save_btn.setBezelStyle_(NSBezelStyleRounded)
156
+ save_btn.setTarget_(self)
157
+ save_btn.setAction_(objc.selector(self.save_, signature=b"v@:@"))
158
+ save_btn.setKeyEquivalent_("\r")
159
+ content.addSubview_(save_btn)
160
+
161
+ y_pos += button_area_height
162
+
163
+ # Text view
164
+ scroll_frame = NSMakeRect(padding, y_pos, window_width - padding * 2, text_area_height)
165
+ scroll_view = NSScrollView.alloc().initWithFrame_(scroll_frame)
166
+ scroll_view.setHasVerticalScroller_(True)
167
+ scroll_view.setBorderType_(1)
168
+
169
+ text_frame = NSMakeRect(0, 0, scroll_frame.size.width - 4, scroll_frame.size.height)
170
+ self.text_view = NSTextView.alloc().initWithFrame_(text_frame)
171
+ self.text_view.setFont_(NSFont.systemFontOfSize_(13))
172
+ scroll_view.setDocumentView_(self.text_view)
173
+ content.addSubview_(scroll_view)
174
+
175
+ y_pos += text_area_height + 5
176
+
177
+ # Label
178
+ label = NSTextField.labelWithString_("Instructions for Claude Code:")
179
+ label.setFrame_(NSMakeRect(padding, y_pos, 300, label_height))
180
+ content.addSubview_(label)
181
+
182
+ y_pos += label_height + 5
183
+
184
+ # Image view
185
+ image_view = NSImageView.alloc().initWithFrame_(NSMakeRect(padding, y_pos, display_width, display_height))
186
+ image_view.setImage_(image)
187
+ image_view.setImageScaling_(NSImageScaleProportionallyDown)
188
+ content.addSubview_(image_view)
189
+
190
+ def show(self):
191
+ """Display the window on top of fullscreen apps."""
192
+ self.window.center()
193
+ self.window.orderFrontRegardless()
194
+ self.window.makeKeyWindow()
195
+ self.window.makeFirstResponder_(self.text_view)
196
+
197
+ def cancel_(self, sender):
198
+ """Handle cancel button click."""
199
+ cleanup_temp_file(self.image_path)
200
+ self.window.close()
201
+
202
+ def save_(self, sender):
203
+ """Handle save button click."""
204
+ instructions = self.text_view.string()
205
+ saved_path = save_screenshot(self.image_path)
206
+
207
+ if instructions and instructions.strip():
208
+ add_task(instructions.strip(), saved_path)
209
+
210
+ self.window.close()
211
+
212
+
213
+ def show_annotation_window(image_path: str) -> None:
214
+ """Display the annotation window for a screenshot."""
215
+ global _current_controller
216
+
217
+ if _current_controller and _current_controller.window:
218
+ _current_controller.window.close()
219
+
220
+ _current_controller = AnnotationWindowController.alloc().initWithImagePath_(image_path)
221
+ if _current_controller:
222
+ _current_controller.show()