-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathscreen.py
More file actions
249 lines (217 loc) · 8.58 KB
/
Copy pathscreen.py
File metadata and controls
249 lines (217 loc) · 8.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
"""
JARVIS Screen Awareness — see what's on the user's screen.
Two capabilities:
1. Window/app list via AppleScript (fast, text-based)
2. Screenshot via screencapture → Claude vision API (sees everything)
"""
import asyncio
import base64
import json
import logging
import tempfile
from pathlib import Path
log = logging.getLogger("jarvis.screen")
async def get_active_windows() -> list[dict]:
"""Get list of visible windows with app name, window title, and position.
Uses AppleScript + System Events to enumerate windows.
Returns list of {"app": str, "title": str, "frontmost": bool}.
"""
# Use a simpler approach that's more permission-friendly
script = """
set windowList to ""
tell application "System Events"
set frontApp to name of first application process whose frontmost is true
set visibleApps to every application process whose visible is true
repeat with proc in visibleApps
set appName to name of proc
try
set winCount to count of windows of proc
if winCount > 0 then
repeat with w in (windows of proc)
try
set winTitle to name of w
if winTitle is not "" and winTitle is not missing value then
set windowList to windowList & appName & "|||" & winTitle & "|||" & (appName = frontApp) & linefeed
end if
end try
end repeat
end if
end try
end repeat
end tell
return windowList
"""
try:
proc = await asyncio.create_subprocess_exec(
"osascript", "-e", script,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await asyncio.wait_for(proc.communicate(), timeout=5)
if proc.returncode != 0:
log.warning(f"get_active_windows failed: {stderr.decode()[:200]}")
return []
windows = []
for line in stdout.decode().strip().split("\n"):
parts = line.strip().split("|||")
if len(parts) >= 3:
windows.append({
"app": parts[0].strip(),
"title": parts[1].strip(),
"frontmost": parts[2].strip().lower() == "true",
})
return windows
except asyncio.TimeoutError:
log.warning("get_active_windows timed out")
return []
except Exception as e:
log.warning(f"get_active_windows error: {e}")
return []
async def get_running_apps() -> list[str]:
"""Get list of running application names (visible only)."""
script = """
tell application "System Events"
set appNames to name of every application process whose visible is true
set output to ""
repeat with a in appNames
set output to output & a & linefeed
end repeat
return output
end tell
"""
try:
proc = await asyncio.create_subprocess_exec(
"osascript", "-e", script,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await asyncio.wait_for(proc.communicate(), timeout=5)
if proc.returncode == 0:
return [a.strip() for a in stdout.decode().strip().split("\n") if a.strip()]
return []
except Exception as e:
log.warning(f"get_running_apps error: {e}")
return []
async def take_screenshot(display_only: bool = True) -> str | None:
"""Take a screenshot and return base64-encoded PNG.
Args:
display_only: If True, capture main display only. If False, all displays.
Returns:
Base64-encoded PNG string, or None on failure.
"""
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as f:
tmp_path = f.name
try:
cmd = ["screencapture", "-x"] # -x = no sound
if display_only:
cmd.append("-m") # main display only
cmd.append(tmp_path)
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
await asyncio.wait_for(proc.communicate(), timeout=10)
if proc.returncode != 0 or not Path(tmp_path).exists():
log.warning("Screenshot capture failed")
return None
data = Path(tmp_path).read_bytes()
log.info(f"Screenshot captured: {len(data)} bytes")
return base64.b64encode(data).decode()
except asyncio.TimeoutError:
log.warning("Screenshot timed out")
return None
except Exception as e:
log.warning(f"Screenshot error: {e}")
return None
finally:
try:
Path(tmp_path).unlink(missing_ok=True)
except Exception:
pass
async def describe_screen(anthropic_client) -> str:
"""Describe what's on the user's screen.
Tries screenshot + vision first. Falls back to window list + LLM summary.
"""
# Try screenshot + vision
screenshot_b64 = await take_screenshot()
if screenshot_b64 and anthropic_client:
try:
response = await anthropic_client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=300,
system=(
"You are JARVIS analyzing a screenshot of the user's desktop. "
"Describe what you see concisely: which apps are open, what the user "
"appears to be working on, any notable content visible. "
"Be specific about app names, file names, URLs, code, or documents visible. "
"2-4 sentences max. No markdown."
),
messages=[{
"role": "user",
"content": [
{
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": screenshot_b64,
},
},
{
"type": "text",
"text": "What's on my screen right now?",
},
],
}],
)
return response.content[0].text
except Exception as e:
log.warning(f"Vision call failed, falling back to window list: {e}")
# Fallback: get window list and have LLM summarize
windows = await get_active_windows()
apps = await get_running_apps()
if not windows and not apps:
return "I wasn't able to see your screen, sir. Screen recording permission may be needed."
# Build a text description for LLM to summarize
context_parts = []
if windows:
for w in windows:
marker = " (ACTIVE)" if w["frontmost"] else ""
context_parts.append(f"{w['app']}: {w['title']}{marker}")
if apps:
window_apps = set(w["app"] for w in windows) if windows else set()
bg_apps = [a for a in apps if a not in window_apps]
if bg_apps:
context_parts.append(f"Background apps: {', '.join(bg_apps)}")
if anthropic_client and context_parts:
try:
response = await anthropic_client.messages.create(
model="claude-haiku-4-5-20251001",
max_tokens=100,
system=(
"You are JARVIS. Given the user's open windows and apps, summarize "
"what they appear to be working on in 1-2 sentences. Natural voice, no markdown."
),
messages=[{"role": "user", "content": "Open windows:\n" + "\n".join(context_parts)}],
)
return response.content[0].text
except Exception:
pass
# Raw fallback
if windows:
active = next((w for w in windows if w["frontmost"]), None)
result = f"You have {len(windows)} windows open across {len(set(w['app'] for w in windows))} apps."
if active:
result += f" Currently focused on {active['app']}: {active['title']}."
return result
return f"Running apps: {', '.join(apps)}. Couldn't read window titles, sir."
def format_windows_for_context(windows: list[dict]) -> str:
"""Format window list as context string for the LLM."""
if not windows:
return ""
lines = ["Currently open on your desktop:"]
for w in windows:
marker = " (active)" if w["frontmost"] else ""
lines.append(f" - {w['app']}: {w['title']}{marker}")
return "\n".join(lines)