devduck 0.1.0__py3-none-any.whl → 0.1.1766644714__py3-none-any.whl
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.
Potentially problematic release.
This version of devduck might be problematic. Click here for more details.
- devduck/__init__.py +1439 -483
- devduck/__main__.py +7 -0
- devduck/_version.py +34 -0
- devduck/agentcore_handler.py +76 -0
- devduck/test_redduck.py +0 -1
- devduck/tools/__init__.py +47 -0
- devduck/tools/_ambient_input.py +423 -0
- devduck/tools/_tray_app.py +530 -0
- devduck/tools/agentcore_agents.py +197 -0
- devduck/tools/agentcore_config.py +441 -0
- devduck/tools/agentcore_invoke.py +423 -0
- devduck/tools/agentcore_logs.py +320 -0
- devduck/tools/ambient.py +157 -0
- devduck/tools/create_subagent.py +659 -0
- devduck/tools/fetch_github_tool.py +201 -0
- devduck/tools/install_tools.py +409 -0
- devduck/tools/ipc.py +546 -0
- devduck/tools/mcp_server.py +600 -0
- devduck/tools/scraper.py +935 -0
- devduck/tools/speech_to_speech.py +850 -0
- devduck/tools/state_manager.py +292 -0
- devduck/tools/store_in_kb.py +187 -0
- devduck/tools/system_prompt.py +608 -0
- devduck/tools/tcp.py +263 -94
- devduck/tools/tray.py +247 -0
- devduck/tools/use_github.py +438 -0
- devduck/tools/websocket.py +498 -0
- devduck-0.1.1766644714.dist-info/METADATA +717 -0
- devduck-0.1.1766644714.dist-info/RECORD +33 -0
- {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/entry_points.txt +1 -0
- devduck-0.1.1766644714.dist-info/licenses/LICENSE +201 -0
- devduck/install.sh +0 -42
- devduck-0.1.0.dist-info/METADATA +0 -106
- devduck-0.1.0.dist-info/RECORD +0 -11
- devduck-0.1.0.dist-info/licenses/LICENSE +0 -21
- {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/WHEEL +0 -0
- {devduck-0.1.0.dist-info → devduck-0.1.1766644714.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevDuck Tray - Modern tray app with server controls & agent capabilities
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import rumps
|
|
6
|
+
import socket
|
|
7
|
+
import threading
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import tempfile
|
|
11
|
+
import webbrowser
|
|
12
|
+
from queue import Queue
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DevDuckTray(rumps.App):
|
|
19
|
+
def __init__(self):
|
|
20
|
+
super().__init__("🦆", quit_button=None)
|
|
21
|
+
|
|
22
|
+
# State
|
|
23
|
+
self.command_queue = Queue()
|
|
24
|
+
self.command_responses = {}
|
|
25
|
+
self.ui_update_queue = Queue()
|
|
26
|
+
self.state = {
|
|
27
|
+
"status": "idle",
|
|
28
|
+
"tcp_enabled": True,
|
|
29
|
+
"ws_enabled": True,
|
|
30
|
+
"mcp_enabled": True,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
self.base_icon = "🦆"
|
|
34
|
+
|
|
35
|
+
# Multi-stream state
|
|
36
|
+
self.active_streams = {}
|
|
37
|
+
self.recent_results = []
|
|
38
|
+
self.max_recent = 5
|
|
39
|
+
|
|
40
|
+
# Persistent menu items
|
|
41
|
+
self.saved_menu_items = []
|
|
42
|
+
|
|
43
|
+
# Build initial menu
|
|
44
|
+
self._build_menu()
|
|
45
|
+
|
|
46
|
+
# Start IPC listener
|
|
47
|
+
self.socket_path = os.path.join(tempfile.gettempdir(), "devduck_tray.sock")
|
|
48
|
+
self._start_ipc_listener()
|
|
49
|
+
|
|
50
|
+
# Start command processor
|
|
51
|
+
self.command_timer = rumps.Timer(self._process_commands, 0.1)
|
|
52
|
+
self.command_timer.start()
|
|
53
|
+
|
|
54
|
+
# Start UI update processor (main thread only)
|
|
55
|
+
self.ui_timer = rumps.Timer(self._process_ui_updates, 0.2)
|
|
56
|
+
self.ui_timer.start()
|
|
57
|
+
|
|
58
|
+
# Import devduck
|
|
59
|
+
try:
|
|
60
|
+
os.environ["DEVDUCK_AUTO_START_SERVERS"] = "false"
|
|
61
|
+
import devduck
|
|
62
|
+
|
|
63
|
+
self.devduck = devduck
|
|
64
|
+
except ImportError:
|
|
65
|
+
self.devduck = None
|
|
66
|
+
|
|
67
|
+
def _process_ui_updates(self, _):
|
|
68
|
+
"""Process UI updates on main thread only"""
|
|
69
|
+
updated = False
|
|
70
|
+
while not self.ui_update_queue.empty():
|
|
71
|
+
update_type = self.ui_update_queue.get()
|
|
72
|
+
if update_type == "menu":
|
|
73
|
+
updated = True
|
|
74
|
+
|
|
75
|
+
if updated:
|
|
76
|
+
self._build_menu()
|
|
77
|
+
|
|
78
|
+
def _request_menu_update(self):
|
|
79
|
+
"""Request menu update (thread-safe)"""
|
|
80
|
+
self.ui_update_queue.put("menu")
|
|
81
|
+
|
|
82
|
+
def _build_menu(self):
|
|
83
|
+
"""Build menu with server controls and agent capabilities"""
|
|
84
|
+
self.menu.clear()
|
|
85
|
+
|
|
86
|
+
# Test Button - First item for easy testing
|
|
87
|
+
self.menu.add(
|
|
88
|
+
rumps.MenuItem(
|
|
89
|
+
"🧪 Test Agent", callback=self._create_callback("what time is it?")
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
self.menu.add(rumps.separator)
|
|
93
|
+
|
|
94
|
+
# Active Streams Section
|
|
95
|
+
if self.active_streams:
|
|
96
|
+
self.menu.add(rumps.MenuItem("🌊 Active Streams", callback=None))
|
|
97
|
+
for stream_id, stream_data in list(self.active_streams.items()):
|
|
98
|
+
icon = {
|
|
99
|
+
"thinking": "🤔",
|
|
100
|
+
"processing": "💡",
|
|
101
|
+
"complete": "✅",
|
|
102
|
+
"error": "❌",
|
|
103
|
+
}.get(stream_data["status"], "💭")
|
|
104
|
+
query_short = (
|
|
105
|
+
stream_data["query"][:30] + "..."
|
|
106
|
+
if len(stream_data["query"]) > 30
|
|
107
|
+
else stream_data["query"]
|
|
108
|
+
)
|
|
109
|
+
text_short = (
|
|
110
|
+
stream_data["text"][:40] + "..."
|
|
111
|
+
if len(stream_data["text"]) > 40
|
|
112
|
+
else stream_data["text"]
|
|
113
|
+
)
|
|
114
|
+
menu_text = f" {icon} {query_short}: {text_short}"
|
|
115
|
+
self.menu.add(rumps.MenuItem(menu_text, callback=None))
|
|
116
|
+
self.menu.add(rumps.separator)
|
|
117
|
+
|
|
118
|
+
# User-defined menu items
|
|
119
|
+
if self.saved_menu_items:
|
|
120
|
+
for item in self.saved_menu_items:
|
|
121
|
+
if item.get("type") == "separator":
|
|
122
|
+
self.menu.add(rumps.separator)
|
|
123
|
+
else:
|
|
124
|
+
label = item.get("title") or item.get("label", "Item")
|
|
125
|
+
query = item.get("query") or item.get("action", label)
|
|
126
|
+
self.menu.add(
|
|
127
|
+
rumps.MenuItem(label, callback=self._create_callback(query))
|
|
128
|
+
)
|
|
129
|
+
self.menu.add(rumps.separator)
|
|
130
|
+
|
|
131
|
+
# Recent Results Section
|
|
132
|
+
if self.recent_results:
|
|
133
|
+
self.menu.add(rumps.MenuItem("📝 Recent Results", callback=None))
|
|
134
|
+
for query, result, timestamp in self.recent_results[: self.max_recent]:
|
|
135
|
+
query_short = query[:25] + "..." if len(query) > 25 else query
|
|
136
|
+
result_short = result[:35] + "..." if len(result) > 35 else result
|
|
137
|
+
time_str = timestamp.strftime("%H:%M")
|
|
138
|
+
menu_text = f" [{time_str}] {query_short} → {result_short}"
|
|
139
|
+
self.menu.add(
|
|
140
|
+
rumps.MenuItem(
|
|
141
|
+
menu_text,
|
|
142
|
+
callback=self._create_show_result_callback(query, result),
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
self.menu.add(rumps.separator)
|
|
146
|
+
|
|
147
|
+
# Status
|
|
148
|
+
self.menu.add(rumps.MenuItem(f"Status: {self.state['status']}", callback=None))
|
|
149
|
+
self.menu.add(rumps.separator)
|
|
150
|
+
|
|
151
|
+
# Server controls
|
|
152
|
+
self.menu.add(rumps.MenuItem("🌐 Servers", callback=None))
|
|
153
|
+
|
|
154
|
+
tcp_status = "✅" if self.state["tcp_enabled"] else "❌"
|
|
155
|
+
self.menu.add(
|
|
156
|
+
rumps.MenuItem(f" {tcp_status} TCP (9999)", callback=self.toggle_tcp)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
ws_status = "✅" if self.state["ws_enabled"] else "❌"
|
|
160
|
+
self.menu.add(
|
|
161
|
+
rumps.MenuItem(f" {ws_status} WebSocket (8080)", callback=self.toggle_ws)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
mcp_status = "✅" if self.state["mcp_enabled"] else "❌"
|
|
165
|
+
self.menu.add(
|
|
166
|
+
rumps.MenuItem(f" {mcp_status} MCP (8000)", callback=self.toggle_mcp)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
self.menu.add(rumps.separator)
|
|
170
|
+
|
|
171
|
+
# Agent Capabilities
|
|
172
|
+
self.menu.add(rumps.MenuItem("🤖 Agent Capabilities", callback=None))
|
|
173
|
+
self.menu.add(
|
|
174
|
+
rumps.MenuItem(
|
|
175
|
+
" 👂 Start Clipboard Listening",
|
|
176
|
+
callback=self._create_callback(
|
|
177
|
+
"start clipboard monitoring in background"
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
self.menu.add(
|
|
182
|
+
rumps.MenuItem(
|
|
183
|
+
" 🎤 Start Background Listening",
|
|
184
|
+
callback=self._create_callback("start background audio listening"),
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
self.menu.add(
|
|
188
|
+
rumps.MenuItem(
|
|
189
|
+
" 📺 Start Screen Reader",
|
|
190
|
+
callback=self._create_callback("start screen reader monitoring"),
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
self.menu.add(
|
|
194
|
+
rumps.MenuItem(
|
|
195
|
+
" 👁️ Start YOLO Vision",
|
|
196
|
+
callback=self._create_callback("start yolo vision detection"),
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
self.menu.add(rumps.separator)
|
|
201
|
+
|
|
202
|
+
# Actions
|
|
203
|
+
self.menu.add(rumps.MenuItem("💻 Show Input", callback=self.show_input))
|
|
204
|
+
self.menu.add(rumps.MenuItem("🌐 Web Dashboard", callback=self.open_dashboard))
|
|
205
|
+
|
|
206
|
+
self.menu.add(rumps.separator)
|
|
207
|
+
self.menu.add(rumps.MenuItem("Quit", callback=self.quit_app))
|
|
208
|
+
|
|
209
|
+
def _create_show_result_callback(self, query, result):
|
|
210
|
+
"""Create callback to show full result in notification"""
|
|
211
|
+
|
|
212
|
+
def callback(sender):
|
|
213
|
+
rumps.notification("DevDuck Result", query, result)
|
|
214
|
+
|
|
215
|
+
return callback
|
|
216
|
+
|
|
217
|
+
def toggle_tcp(self, sender):
|
|
218
|
+
"""Toggle TCP server"""
|
|
219
|
+
if self.devduck and hasattr(self.devduck.devduck.agent, "tool"):
|
|
220
|
+
try:
|
|
221
|
+
action = "stop_server" if self.state["tcp_enabled"] else "start_server"
|
|
222
|
+
self.devduck.devduck.agent.tool.tcp(action=action, port=9999)
|
|
223
|
+
self.state["tcp_enabled"] = not self.state["tcp_enabled"]
|
|
224
|
+
self._request_menu_update()
|
|
225
|
+
rumps.notification(
|
|
226
|
+
"DevDuck",
|
|
227
|
+
"",
|
|
228
|
+
f"TCP: {'ON' if self.state['tcp_enabled'] else 'OFF'}",
|
|
229
|
+
)
|
|
230
|
+
except Exception as e:
|
|
231
|
+
rumps.notification("DevDuck Error", "", str(e))
|
|
232
|
+
|
|
233
|
+
def toggle_ws(self, sender):
|
|
234
|
+
"""Toggle WebSocket server"""
|
|
235
|
+
if self.devduck and hasattr(self.devduck.devduck.agent, "tool"):
|
|
236
|
+
try:
|
|
237
|
+
action = "stop_server" if self.state["ws_enabled"] else "start_server"
|
|
238
|
+
self.devduck.devduck.agent.tool.websocket(action=action, port=8080)
|
|
239
|
+
self.state["ws_enabled"] = not self.state["ws_enabled"]
|
|
240
|
+
self._request_menu_update()
|
|
241
|
+
rumps.notification(
|
|
242
|
+
"DevDuck",
|
|
243
|
+
"",
|
|
244
|
+
f"WebSocket: {'ON' if self.state['ws_enabled'] else 'OFF'}",
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
rumps.notification("DevDuck Error", "", str(e))
|
|
248
|
+
|
|
249
|
+
def toggle_mcp(self, sender):
|
|
250
|
+
"""Toggle MCP server"""
|
|
251
|
+
if self.devduck and hasattr(self.devduck.devduck.agent, "tool"):
|
|
252
|
+
try:
|
|
253
|
+
action = "stop" if self.state["mcp_enabled"] else "start"
|
|
254
|
+
self.devduck.devduck.agent.tool.mcp_server(action=action, port=8000)
|
|
255
|
+
self.state["mcp_enabled"] = not self.state["mcp_enabled"]
|
|
256
|
+
self._request_menu_update()
|
|
257
|
+
rumps.notification(
|
|
258
|
+
"DevDuck",
|
|
259
|
+
"",
|
|
260
|
+
f"MCP: {'ON' if self.state['mcp_enabled'] else 'OFF'}",
|
|
261
|
+
)
|
|
262
|
+
except Exception as e:
|
|
263
|
+
rumps.notification("DevDuck Error", "", str(e))
|
|
264
|
+
|
|
265
|
+
def show_input(self, sender):
|
|
266
|
+
"""Show ambient input overlay"""
|
|
267
|
+
ambient_socket = os.path.join(tempfile.gettempdir(), "devduck_ambient.sock")
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
271
|
+
client.settimeout(2.0)
|
|
272
|
+
client.connect(ambient_socket)
|
|
273
|
+
client.send(json.dumps({"action": "show"}).encode("utf-8"))
|
|
274
|
+
client.close()
|
|
275
|
+
except:
|
|
276
|
+
# Ambient not running - try to start via devduck
|
|
277
|
+
if self.devduck and hasattr(self.devduck.devduck.agent, "tool"):
|
|
278
|
+
try:
|
|
279
|
+
self.devduck.devduck.agent.tool.ambient(action="start")
|
|
280
|
+
rumps.notification("DevDuck", "", "Input overlay started! 💻")
|
|
281
|
+
except Exception as e:
|
|
282
|
+
rumps.notification("DevDuck Error", "", f"Failed: {e}")
|
|
283
|
+
|
|
284
|
+
def open_dashboard(self, sender):
|
|
285
|
+
"""Open web dashboard"""
|
|
286
|
+
webbrowser.open("https://cagataycali.github.io/devduck")
|
|
287
|
+
|
|
288
|
+
def quit_app(self, sender):
|
|
289
|
+
"""Quit application"""
|
|
290
|
+
rumps.quit_application()
|
|
291
|
+
|
|
292
|
+
def _start_ipc_listener(self):
|
|
293
|
+
"""Start Unix socket listener"""
|
|
294
|
+
|
|
295
|
+
def listener():
|
|
296
|
+
try:
|
|
297
|
+
if os.path.exists(self.socket_path):
|
|
298
|
+
os.unlink(self.socket_path)
|
|
299
|
+
|
|
300
|
+
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
301
|
+
server.bind(self.socket_path)
|
|
302
|
+
server.listen(5)
|
|
303
|
+
|
|
304
|
+
while True:
|
|
305
|
+
conn, _ = server.accept()
|
|
306
|
+
threading.Thread(
|
|
307
|
+
target=self._handle_connection, args=(conn,), daemon=True
|
|
308
|
+
).start()
|
|
309
|
+
except Exception as e:
|
|
310
|
+
print(f"IPC error: {e}")
|
|
311
|
+
|
|
312
|
+
threading.Thread(target=listener, daemon=True).start()
|
|
313
|
+
|
|
314
|
+
def _handle_connection(self, conn):
|
|
315
|
+
"""Handle IPC connection"""
|
|
316
|
+
try:
|
|
317
|
+
data = conn.recv(4096)
|
|
318
|
+
if data:
|
|
319
|
+
command = json.loads(data.decode("utf-8"))
|
|
320
|
+
request_id = command.get("request_id", id(command))
|
|
321
|
+
|
|
322
|
+
self.command_queue.put((request_id, command))
|
|
323
|
+
|
|
324
|
+
# Wait for response
|
|
325
|
+
import time
|
|
326
|
+
|
|
327
|
+
timeout = 5.0
|
|
328
|
+
start = time.time()
|
|
329
|
+
while time.time() - start < timeout:
|
|
330
|
+
if request_id in self.command_responses:
|
|
331
|
+
response = self.command_responses.pop(request_id)
|
|
332
|
+
conn.send(json.dumps(response).encode("utf-8"))
|
|
333
|
+
break
|
|
334
|
+
time.sleep(0.05)
|
|
335
|
+
else:
|
|
336
|
+
conn.send(
|
|
337
|
+
json.dumps({"status": "error", "message": "timeout"}).encode(
|
|
338
|
+
"utf-8"
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
except Exception as e:
|
|
342
|
+
try:
|
|
343
|
+
conn.send(
|
|
344
|
+
json.dumps({"status": "error", "message": str(e)}).encode("utf-8")
|
|
345
|
+
)
|
|
346
|
+
except:
|
|
347
|
+
pass
|
|
348
|
+
finally:
|
|
349
|
+
conn.close()
|
|
350
|
+
|
|
351
|
+
def _process_commands(self, _):
|
|
352
|
+
"""Process queued commands on main thread"""
|
|
353
|
+
while not self.command_queue.empty():
|
|
354
|
+
request_id, cmd = self.command_queue.get()
|
|
355
|
+
response = self._handle_command(cmd)
|
|
356
|
+
self.command_responses[request_id] = response
|
|
357
|
+
|
|
358
|
+
def _handle_command(self, cmd):
|
|
359
|
+
"""Handle IPC commands"""
|
|
360
|
+
try:
|
|
361
|
+
action = cmd.get("action")
|
|
362
|
+
|
|
363
|
+
if action == "update_title":
|
|
364
|
+
new_icon = cmd.get("title", "🦆")
|
|
365
|
+
self.base_icon = new_icon
|
|
366
|
+
self.title = self.base_icon
|
|
367
|
+
return {"status": "success"}
|
|
368
|
+
|
|
369
|
+
elif action == "set_progress":
|
|
370
|
+
progress = cmd.get("progress", "idle")
|
|
371
|
+
icons = {
|
|
372
|
+
"idle": "🦆",
|
|
373
|
+
"thinking": "🤔",
|
|
374
|
+
"processing": "💡",
|
|
375
|
+
"complete": "✅",
|
|
376
|
+
"error": "❌",
|
|
377
|
+
}
|
|
378
|
+
self.base_icon = icons.get(progress, "🦆")
|
|
379
|
+
self.title = self.base_icon
|
|
380
|
+
return {"status": "success"}
|
|
381
|
+
|
|
382
|
+
elif action == "update_menu":
|
|
383
|
+
items = cmd.get("items", [])
|
|
384
|
+
self.saved_menu_items = items
|
|
385
|
+
self._request_menu_update()
|
|
386
|
+
return {"status": "success"}
|
|
387
|
+
|
|
388
|
+
elif action == "notify":
|
|
389
|
+
msg = cmd.get("message", {})
|
|
390
|
+
rumps.notification(
|
|
391
|
+
msg.get("title", "DevDuck"),
|
|
392
|
+
msg.get("subtitle", ""),
|
|
393
|
+
msg.get("message", ""),
|
|
394
|
+
)
|
|
395
|
+
return {"status": "success"}
|
|
396
|
+
|
|
397
|
+
elif action == "stream_text":
|
|
398
|
+
text = cmd.get("text", "")
|
|
399
|
+
stream_id = cmd.get("stream_id", "default")
|
|
400
|
+
status = cmd.get("status", "processing")
|
|
401
|
+
query = cmd.get("query", "")
|
|
402
|
+
|
|
403
|
+
self.active_streams[stream_id] = {
|
|
404
|
+
"query": query,
|
|
405
|
+
"status": status,
|
|
406
|
+
"text": text,
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
self._request_menu_update()
|
|
410
|
+
|
|
411
|
+
return {"status": "success"}
|
|
412
|
+
|
|
413
|
+
elif action == "stream_complete":
|
|
414
|
+
stream_id = cmd.get("stream_id", "default")
|
|
415
|
+
if stream_id in self.active_streams:
|
|
416
|
+
stream_data = self.active_streams.pop(stream_id)
|
|
417
|
+
self.recent_results.insert(
|
|
418
|
+
0, (stream_data["query"], stream_data["text"], datetime.now())
|
|
419
|
+
)
|
|
420
|
+
self.recent_results = self.recent_results[: self.max_recent]
|
|
421
|
+
self._request_menu_update()
|
|
422
|
+
return {"status": "success"}
|
|
423
|
+
|
|
424
|
+
elif action == "show_input":
|
|
425
|
+
self.show_input(None)
|
|
426
|
+
return {"status": "success"}
|
|
427
|
+
|
|
428
|
+
elif action in ["toggle_tcp", "toggle_ws", "toggle_mcp"]:
|
|
429
|
+
if action == "toggle_tcp":
|
|
430
|
+
self.toggle_tcp(None)
|
|
431
|
+
elif action == "toggle_ws":
|
|
432
|
+
self.toggle_ws(None)
|
|
433
|
+
elif action == "toggle_mcp":
|
|
434
|
+
self.toggle_mcp(None)
|
|
435
|
+
return {"status": "success"}
|
|
436
|
+
|
|
437
|
+
return {"status": "error", "message": "Unknown action"}
|
|
438
|
+
except Exception as e:
|
|
439
|
+
return {"status": "error", "message": str(e)}
|
|
440
|
+
|
|
441
|
+
def _create_callback(self, query):
|
|
442
|
+
"""Create callback for menu item"""
|
|
443
|
+
|
|
444
|
+
def callback(sender):
|
|
445
|
+
if self.devduck:
|
|
446
|
+
stream_id = str(uuid.uuid4())[:8]
|
|
447
|
+
|
|
448
|
+
self.active_streams[stream_id] = {
|
|
449
|
+
"query": query,
|
|
450
|
+
"status": "thinking",
|
|
451
|
+
"text": "Starting...",
|
|
452
|
+
}
|
|
453
|
+
self._request_menu_update()
|
|
454
|
+
|
|
455
|
+
self.base_icon = "🤔"
|
|
456
|
+
self.title = self.base_icon
|
|
457
|
+
|
|
458
|
+
def run():
|
|
459
|
+
try:
|
|
460
|
+
self.active_streams[stream_id]["status"] = "processing"
|
|
461
|
+
self.active_streams[stream_id]["text"] = "Processing query..."
|
|
462
|
+
self._request_menu_update()
|
|
463
|
+
|
|
464
|
+
self.base_icon = "💡"
|
|
465
|
+
self.title = self.base_icon
|
|
466
|
+
|
|
467
|
+
result = self.devduck.ask(query)
|
|
468
|
+
result_str = str(result)
|
|
469
|
+
|
|
470
|
+
self.active_streams[stream_id]["status"] = "complete"
|
|
471
|
+
self.active_streams[stream_id]["text"] = result_str
|
|
472
|
+
self._request_menu_update()
|
|
473
|
+
|
|
474
|
+
self.base_icon = "✅"
|
|
475
|
+
self.title = self.base_icon
|
|
476
|
+
|
|
477
|
+
rumps.notification("DevDuck Result", query, result_str)
|
|
478
|
+
|
|
479
|
+
def move_to_recent():
|
|
480
|
+
if stream_id in self.active_streams:
|
|
481
|
+
stream_data = self.active_streams.pop(stream_id)
|
|
482
|
+
self.recent_results.insert(
|
|
483
|
+
0,
|
|
484
|
+
(
|
|
485
|
+
stream_data["query"],
|
|
486
|
+
stream_data["text"],
|
|
487
|
+
datetime.now(),
|
|
488
|
+
),
|
|
489
|
+
)
|
|
490
|
+
self.recent_results = self.recent_results[
|
|
491
|
+
: self.max_recent
|
|
492
|
+
]
|
|
493
|
+
self._request_menu_update()
|
|
494
|
+
self._reset_icon()
|
|
495
|
+
|
|
496
|
+
threading.Timer(3.0, move_to_recent).start()
|
|
497
|
+
|
|
498
|
+
except Exception as e:
|
|
499
|
+
self.active_streams[stream_id]["status"] = "error"
|
|
500
|
+
self.active_streams[stream_id]["text"] = str(e)
|
|
501
|
+
self._request_menu_update()
|
|
502
|
+
|
|
503
|
+
self.base_icon = "❌"
|
|
504
|
+
self.title = self.base_icon
|
|
505
|
+
rumps.notification("DevDuck Error", query, str(e))
|
|
506
|
+
|
|
507
|
+
threading.Timer(
|
|
508
|
+
3.0, lambda: self._cleanup_stream(stream_id)
|
|
509
|
+
).start()
|
|
510
|
+
|
|
511
|
+
threading.Thread(target=run, daemon=True).start()
|
|
512
|
+
|
|
513
|
+
return callback
|
|
514
|
+
|
|
515
|
+
def _cleanup_stream(self, stream_id):
|
|
516
|
+
"""Remove stream and reset icon"""
|
|
517
|
+
if stream_id in self.active_streams:
|
|
518
|
+
self.active_streams.pop(stream_id)
|
|
519
|
+
self._request_menu_update()
|
|
520
|
+
self._reset_icon()
|
|
521
|
+
|
|
522
|
+
def _reset_icon(self):
|
|
523
|
+
"""Reset icon to default"""
|
|
524
|
+
self.base_icon = "🦆"
|
|
525
|
+
self.title = self.base_icon
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
if __name__ == "__main__":
|
|
529
|
+
app = DevDuckTray()
|
|
530
|
+
app.run()
|