devduck 0.4.1__py3-none-any.whl → 0.5.0__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 +35 -1
- devduck/_version.py +2 -2
- devduck/tools/__init__.py +3 -1
- devduck/tools/_ambient_input.py +423 -0
- devduck/tools/_tray_app.py +522 -0
- devduck/tools/ambient.py +157 -0
- devduck/tools/ipc.py +543 -0
- devduck/tools/tcp.py +0 -4
- devduck/tools/tray.py +246 -0
- devduck-0.5.0.dist-info/METADATA +554 -0
- devduck-0.5.0.dist-info/RECORD +24 -0
- {devduck-0.4.1.dist-info → devduck-0.5.0.dist-info}/entry_points.txt +1 -0
- devduck-0.5.0.dist-info/licenses/LICENSE +201 -0
- devduck-0.4.1.dist-info/METADATA +0 -283
- devduck-0.4.1.dist-info/RECORD +0 -19
- devduck-0.4.1.dist-info/licenses/LICENSE +0 -21
- {devduck-0.4.1.dist-info → devduck-0.5.0.dist-info}/WHEEL +0 -0
- {devduck-0.4.1.dist-info → devduck-0.5.0.dist-info}/top_level.txt +0 -0
devduck/__init__.py
CHANGED
|
@@ -18,6 +18,7 @@ from logging.handlers import RotatingFileHandler
|
|
|
18
18
|
|
|
19
19
|
os.environ["BYPASS_TOOL_CONSENT"] = "true"
|
|
20
20
|
os.environ["STRANDS_TOOL_CONSOLE_MODE"] = "enabled"
|
|
21
|
+
os.environ["EDITOR_DISABLE_BACKUP"] = "true"
|
|
21
22
|
|
|
22
23
|
# 📝 Setup logging system
|
|
23
24
|
LOG_DIR = Path(tempfile.gettempdir()) / "devduck" / "logs"
|
|
@@ -126,6 +127,7 @@ def get_own_source_code():
|
|
|
126
127
|
except Exception as e:
|
|
127
128
|
return f"Error reading own source code: {e}"
|
|
128
129
|
|
|
130
|
+
|
|
129
131
|
def view_logs_tool(
|
|
130
132
|
action: str = "view",
|
|
131
133
|
lines: int = 100,
|
|
@@ -450,9 +452,11 @@ class DevDuck:
|
|
|
450
452
|
tcp_port=9999,
|
|
451
453
|
ws_port=8080,
|
|
452
454
|
mcp_port=8000,
|
|
455
|
+
ipc_socket=None,
|
|
453
456
|
enable_tcp=True,
|
|
454
457
|
enable_ws=True,
|
|
455
458
|
enable_mcp=True,
|
|
459
|
+
enable_ipc=True,
|
|
456
460
|
):
|
|
457
461
|
"""Initialize the minimalist adaptive agent"""
|
|
458
462
|
logger.info("Initializing DevDuck agent...")
|
|
@@ -494,24 +498,30 @@ class DevDuck:
|
|
|
494
498
|
from .tools import (
|
|
495
499
|
tcp,
|
|
496
500
|
websocket,
|
|
501
|
+
ipc,
|
|
497
502
|
mcp_server,
|
|
498
503
|
install_tools,
|
|
499
504
|
use_github,
|
|
500
505
|
create_subagent,
|
|
501
506
|
store_in_kb,
|
|
502
507
|
system_prompt,
|
|
508
|
+
tray,
|
|
509
|
+
ambient,
|
|
503
510
|
)
|
|
504
511
|
|
|
505
512
|
core_tools.extend(
|
|
506
513
|
[
|
|
507
514
|
tcp,
|
|
508
515
|
websocket,
|
|
516
|
+
ipc,
|
|
509
517
|
mcp_server,
|
|
510
518
|
install_tools,
|
|
511
519
|
use_github,
|
|
512
520
|
create_subagent,
|
|
513
521
|
store_in_kb,
|
|
514
|
-
system_prompt
|
|
522
|
+
system_prompt,
|
|
523
|
+
tray,
|
|
524
|
+
ambient,
|
|
515
525
|
]
|
|
516
526
|
)
|
|
517
527
|
except ImportError as e:
|
|
@@ -542,6 +552,8 @@ class DevDuck:
|
|
|
542
552
|
from strands_tools import (
|
|
543
553
|
shell,
|
|
544
554
|
editor,
|
|
555
|
+
file_read,
|
|
556
|
+
file_write,
|
|
545
557
|
calculator,
|
|
546
558
|
# python_repl,
|
|
547
559
|
image_reader,
|
|
@@ -556,6 +568,8 @@ class DevDuck:
|
|
|
556
568
|
[
|
|
557
569
|
shell,
|
|
558
570
|
editor,
|
|
571
|
+
file_read,
|
|
572
|
+
file_write,
|
|
559
573
|
calculator,
|
|
560
574
|
# python_repl,
|
|
561
575
|
image_reader,
|
|
@@ -670,6 +684,22 @@ class DevDuck:
|
|
|
670
684
|
logger.error(f"Failed to start MCP server: {e}")
|
|
671
685
|
print(f"🦆 ⚠ MCP server failed: {e}")
|
|
672
686
|
|
|
687
|
+
if enable_ipc:
|
|
688
|
+
try:
|
|
689
|
+
# Start IPC server for local process communication
|
|
690
|
+
ipc_socket_path = ipc_socket or "/tmp/devduck_main.sock"
|
|
691
|
+
ipc_result = self.agent.tool.ipc(
|
|
692
|
+
action="start_server", socket_path=ipc_socket_path
|
|
693
|
+
)
|
|
694
|
+
if ipc_result.get("status") == "success":
|
|
695
|
+
logger.info(f"✓ IPC server started on {ipc_socket_path}")
|
|
696
|
+
print(f"🦆 ✓ IPC server: {ipc_socket_path}")
|
|
697
|
+
else:
|
|
698
|
+
logger.warning(f"IPC server start issue: {ipc_result}")
|
|
699
|
+
except Exception as e:
|
|
700
|
+
logger.error(f"Failed to start IPC server: {e}")
|
|
701
|
+
print(f"🦆 ⚠ IPC server failed: {e}")
|
|
702
|
+
|
|
673
703
|
# Start file watcher for auto hot-reload
|
|
674
704
|
self._start_file_watcher()
|
|
675
705
|
|
|
@@ -1121,18 +1151,22 @@ if "--mcp" in sys.argv:
|
|
|
1121
1151
|
_tcp_port = int(os.getenv("DEVDUCK_TCP_PORT", "9999"))
|
|
1122
1152
|
_ws_port = int(os.getenv("DEVDUCK_WS_PORT", "8080"))
|
|
1123
1153
|
_mcp_port = int(os.getenv("DEVDUCK_MCP_PORT", "8000"))
|
|
1154
|
+
_ipc_socket = os.getenv("DEVDUCK_IPC_SOCKET", None)
|
|
1124
1155
|
_enable_tcp = os.getenv("DEVDUCK_ENABLE_TCP", "true").lower() == "true"
|
|
1125
1156
|
_enable_ws = os.getenv("DEVDUCK_ENABLE_WS", "true").lower() == "true"
|
|
1126
1157
|
_enable_mcp = os.getenv("DEVDUCK_ENABLE_MCP", "true").lower() == "true"
|
|
1158
|
+
_enable_ipc = os.getenv("DEVDUCK_ENABLE_IPC", "true").lower() == "true"
|
|
1127
1159
|
|
|
1128
1160
|
devduck = DevDuck(
|
|
1129
1161
|
auto_start_servers=_auto_start,
|
|
1130
1162
|
tcp_port=_tcp_port,
|
|
1131
1163
|
ws_port=_ws_port,
|
|
1132
1164
|
mcp_port=_mcp_port,
|
|
1165
|
+
ipc_socket=_ipc_socket,
|
|
1133
1166
|
enable_tcp=_enable_tcp,
|
|
1134
1167
|
enable_ws=_enable_ws,
|
|
1135
1168
|
enable_mcp=_enable_mcp,
|
|
1169
|
+
enable_ipc=_enable_ipc,
|
|
1136
1170
|
)
|
|
1137
1171
|
|
|
1138
1172
|
|
devduck/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.5.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 5, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
devduck/tools/__init__.py
CHANGED
|
@@ -3,5 +3,7 @@
|
|
|
3
3
|
from .tcp import tcp
|
|
4
4
|
from .mcp_server import mcp_server
|
|
5
5
|
from .install_tools import install_tools
|
|
6
|
+
from .tray import tray
|
|
7
|
+
from .ambient import ambient
|
|
6
8
|
|
|
7
|
-
__all__ = ["tcp", "mcp_server", "install_tools"]
|
|
9
|
+
__all__ = ["tcp", "mcp_server", "install_tools", "tray", "ambient"]
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
"""
|
|
2
|
+
🦆 DevDuck Ambient Input - Preserves scroll position
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import tkinter as tk
|
|
6
|
+
from tkinter import font as tkfont, scrolledtext
|
|
7
|
+
import threading
|
|
8
|
+
import json
|
|
9
|
+
import tempfile
|
|
10
|
+
import queue
|
|
11
|
+
import socket as unix_socket
|
|
12
|
+
import os
|
|
13
|
+
import uuid
|
|
14
|
+
import subprocess
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IPCClient:
|
|
18
|
+
"""IPC client for connecting to devduck main server"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, socket_path="/tmp/devduck_main.sock"):
|
|
21
|
+
self.socket_path = socket_path
|
|
22
|
+
self.sock = None
|
|
23
|
+
self.connected = False
|
|
24
|
+
self.message_queue = queue.Queue()
|
|
25
|
+
|
|
26
|
+
def connect(self):
|
|
27
|
+
try:
|
|
28
|
+
self.sock = unix_socket.socket(unix_socket.AF_UNIX, unix_socket.SOCK_STREAM)
|
|
29
|
+
self.sock.connect(self.socket_path)
|
|
30
|
+
self.connected = True
|
|
31
|
+
threading.Thread(target=self._listen, daemon=True).start()
|
|
32
|
+
return True
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"Failed to connect: {e}")
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def send_message(self, message, turn_id=None):
|
|
38
|
+
if not self.connected:
|
|
39
|
+
print("Not connected!")
|
|
40
|
+
return False
|
|
41
|
+
try:
|
|
42
|
+
data = {"message": message, "turn_id": turn_id or str(uuid.uuid4())}
|
|
43
|
+
msg = json.dumps(data).encode() + b"\n"
|
|
44
|
+
self.sock.sendall(msg)
|
|
45
|
+
print(f"✓ Sent: {message[:50]} [turn: {data['turn_id'][:8]}]")
|
|
46
|
+
return True
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"Send failed: {e}")
|
|
49
|
+
self.connected = False
|
|
50
|
+
return False
|
|
51
|
+
|
|
52
|
+
def _listen(self):
|
|
53
|
+
buffer = b""
|
|
54
|
+
while self.connected:
|
|
55
|
+
try:
|
|
56
|
+
chunk = self.sock.recv(4096)
|
|
57
|
+
if not chunk:
|
|
58
|
+
self.connected = False
|
|
59
|
+
break
|
|
60
|
+
buffer += chunk
|
|
61
|
+
while b"\n" in buffer:
|
|
62
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
63
|
+
try:
|
|
64
|
+
msg = json.loads(line.decode())
|
|
65
|
+
self.message_queue.put(msg)
|
|
66
|
+
except json.JSONDecodeError:
|
|
67
|
+
continue
|
|
68
|
+
except Exception as e:
|
|
69
|
+
print(f"Listen error: {e}")
|
|
70
|
+
self.connected = False
|
|
71
|
+
break
|
|
72
|
+
|
|
73
|
+
def disconnect(self):
|
|
74
|
+
self.connected = False
|
|
75
|
+
if self.sock:
|
|
76
|
+
try:
|
|
77
|
+
self.sock.close()
|
|
78
|
+
except:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class MinimalAmbientInput:
|
|
83
|
+
def __init__(self):
|
|
84
|
+
self.root = tk.Tk()
|
|
85
|
+
self.root.title("🦆 devduck")
|
|
86
|
+
|
|
87
|
+
self.root.attributes("-topmost", True)
|
|
88
|
+
self.root.attributes("-alpha", 0.85)
|
|
89
|
+
|
|
90
|
+
screen_width = self.root.winfo_screenwidth()
|
|
91
|
+
screen_height = self.root.winfo_screenheight()
|
|
92
|
+
|
|
93
|
+
# Better size - bigger and more usable
|
|
94
|
+
window_width = 900
|
|
95
|
+
window_height = 650
|
|
96
|
+
|
|
97
|
+
# CENTER on screen
|
|
98
|
+
x = (screen_width - window_width) // 2
|
|
99
|
+
y = (screen_height - window_height) // 2
|
|
100
|
+
|
|
101
|
+
self.root.geometry(f"{window_width}x{window_height}+{x}+{y}")
|
|
102
|
+
self.root.configure(bg="#00ff88")
|
|
103
|
+
|
|
104
|
+
# Inner frame
|
|
105
|
+
inner_frame = tk.Frame(self.root, bg="#000000")
|
|
106
|
+
inner_frame.pack(fill=tk.BOTH, expand=True, padx=3, pady=3)
|
|
107
|
+
|
|
108
|
+
# Output area - NORMAL state for copying, but read-only via bindings
|
|
109
|
+
self.text = scrolledtext.ScrolledText(
|
|
110
|
+
inner_frame,
|
|
111
|
+
font=tkfont.Font(family="-apple-system", size=14),
|
|
112
|
+
bg="#000000",
|
|
113
|
+
fg="#ffffff",
|
|
114
|
+
insertbackground="#00ff88",
|
|
115
|
+
bd=0,
|
|
116
|
+
highlightthickness=0,
|
|
117
|
+
wrap=tk.WORD,
|
|
118
|
+
padx=10,
|
|
119
|
+
pady=10,
|
|
120
|
+
state="normal", # Keep normal for selection/copying
|
|
121
|
+
height=20,
|
|
122
|
+
)
|
|
123
|
+
self.text.pack(fill=tk.BOTH, expand=True)
|
|
124
|
+
|
|
125
|
+
# Make text read-only by blocking all modification events
|
|
126
|
+
self.text.bind("<Key>", lambda e: "break") # Block keyboard input
|
|
127
|
+
self.text.bind(
|
|
128
|
+
"<Button-2>", lambda e: "break"
|
|
129
|
+
) # Block middle-click paste (Linux)
|
|
130
|
+
self.text.bind("<Button-3>", lambda e: None) # Allow right-click
|
|
131
|
+
|
|
132
|
+
# Input frame at bottom - bigger and more prominent
|
|
133
|
+
input_frame = tk.Frame(inner_frame, bg="#000000", height=90)
|
|
134
|
+
input_frame.pack(fill=tk.X, side=tk.BOTTOM, pady=(10, 0))
|
|
135
|
+
input_frame.pack_propagate(False)
|
|
136
|
+
|
|
137
|
+
# Container for prompt + input
|
|
138
|
+
input_container = tk.Frame(input_frame, bg="#000000")
|
|
139
|
+
input_container.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
|
|
140
|
+
|
|
141
|
+
# Green prompt label - bigger
|
|
142
|
+
prompt_label = tk.Label(
|
|
143
|
+
input_container,
|
|
144
|
+
text=">",
|
|
145
|
+
font=tkfont.Font(family="-apple-system", size=20, weight="bold"),
|
|
146
|
+
fg="#00ff88",
|
|
147
|
+
bg="#000000",
|
|
148
|
+
)
|
|
149
|
+
prompt_label.pack(side=tk.LEFT, padx=(0, 10))
|
|
150
|
+
|
|
151
|
+
# Input entry - bigger, more visible
|
|
152
|
+
self.input_entry = tk.Entry(
|
|
153
|
+
input_container,
|
|
154
|
+
font=tkfont.Font(family="-apple-system", size=18),
|
|
155
|
+
bg="#1a1a1a",
|
|
156
|
+
fg="#ffffff",
|
|
157
|
+
insertbackground="#00ff88",
|
|
158
|
+
bd=0,
|
|
159
|
+
highlightthickness=2,
|
|
160
|
+
highlightbackground="#00ff88",
|
|
161
|
+
highlightcolor="#00ff88",
|
|
162
|
+
)
|
|
163
|
+
self.input_entry.pack(fill=tk.BOTH, expand=True, side=tk.LEFT, ipady=10)
|
|
164
|
+
|
|
165
|
+
# Tags for output - Green user text, no ugly background
|
|
166
|
+
self.text.tag_config(
|
|
167
|
+
"user",
|
|
168
|
+
foreground="#00ff88",
|
|
169
|
+
background="#000000",
|
|
170
|
+
font=tkfont.Font(family="-apple-system", size=14, weight="bold"),
|
|
171
|
+
spacing1=5,
|
|
172
|
+
spacing3=5,
|
|
173
|
+
lmargin1=0,
|
|
174
|
+
lmargin2=0,
|
|
175
|
+
rmargin=0,
|
|
176
|
+
)
|
|
177
|
+
self.text.tag_config(
|
|
178
|
+
"assistant",
|
|
179
|
+
foreground="#ffffff",
|
|
180
|
+
background="#000000",
|
|
181
|
+
spacing1=0,
|
|
182
|
+
spacing3=0,
|
|
183
|
+
lmargin1=0,
|
|
184
|
+
lmargin2=0,
|
|
185
|
+
rmargin=0,
|
|
186
|
+
)
|
|
187
|
+
self.text.tag_config(
|
|
188
|
+
"tool",
|
|
189
|
+
foreground="#00ff88",
|
|
190
|
+
font=tkfont.Font(family="-apple-system", size=12),
|
|
191
|
+
)
|
|
192
|
+
self.text.tag_config("error", foreground="#ef4444")
|
|
193
|
+
|
|
194
|
+
# Bindings
|
|
195
|
+
self.input_entry.bind("<Return>", self.on_enter)
|
|
196
|
+
self.input_entry.bind("<Escape>", lambda e: self.root.withdraw())
|
|
197
|
+
|
|
198
|
+
# State - Turn tracking
|
|
199
|
+
self.ipc_client = IPCClient()
|
|
200
|
+
self.turns = {}
|
|
201
|
+
self.turn_order = []
|
|
202
|
+
|
|
203
|
+
# Command listener
|
|
204
|
+
self.command_socket_path = os.path.join(
|
|
205
|
+
tempfile.gettempdir(), "devduck_ambient.sock"
|
|
206
|
+
)
|
|
207
|
+
self._start_command_listener()
|
|
208
|
+
|
|
209
|
+
# Start
|
|
210
|
+
self.root.after(50, self.process_messages)
|
|
211
|
+
self.root.after(1000, self._try_connect)
|
|
212
|
+
|
|
213
|
+
# Force layout update before showing
|
|
214
|
+
self.root.update_idletasks()
|
|
215
|
+
|
|
216
|
+
self.root.deiconify()
|
|
217
|
+
self._force_focus()
|
|
218
|
+
|
|
219
|
+
def _try_connect(self):
|
|
220
|
+
if self.ipc_client.connect():
|
|
221
|
+
print("✓ Connected to IPC server")
|
|
222
|
+
self._append_text("✓ Connected to DevDuck\n\n", "tool")
|
|
223
|
+
else:
|
|
224
|
+
print("✗ Connection failed, retrying...")
|
|
225
|
+
self.root.after(5000, self._try_connect)
|
|
226
|
+
|
|
227
|
+
def _append_text(self, text, tag=None):
|
|
228
|
+
"""Append text - NO auto-scroll, but stays in normal state"""
|
|
229
|
+
if tag:
|
|
230
|
+
self.text.insert(tk.END, text, tag)
|
|
231
|
+
else:
|
|
232
|
+
self.text.insert(tk.END, text)
|
|
233
|
+
# NO self.text.see(tk.END) - user controls scroll
|
|
234
|
+
|
|
235
|
+
def _render_all_turns(self):
|
|
236
|
+
"""Render all turns - PRESERVES scroll position"""
|
|
237
|
+
# Save current scroll position BEFORE deleting
|
|
238
|
+
try:
|
|
239
|
+
yview = self.text.yview()
|
|
240
|
+
scroll_position = yview[0] # Top of visible area
|
|
241
|
+
except:
|
|
242
|
+
scroll_position = 0.0
|
|
243
|
+
|
|
244
|
+
# Delete and re-render
|
|
245
|
+
self.text.delete("1.0", tk.END)
|
|
246
|
+
|
|
247
|
+
for turn_id in self.turn_order:
|
|
248
|
+
if turn_id in self.turns:
|
|
249
|
+
turn = self.turns[turn_id]
|
|
250
|
+
|
|
251
|
+
# User message with "> " prefix
|
|
252
|
+
query = turn.get("query", "")
|
|
253
|
+
if query:
|
|
254
|
+
self.text.insert(tk.END, f"> {query}\n\n", "user")
|
|
255
|
+
|
|
256
|
+
# Assistant response
|
|
257
|
+
buffer = turn.get("buffer", "")
|
|
258
|
+
if buffer:
|
|
259
|
+
self.text.insert(tk.END, buffer, "assistant")
|
|
260
|
+
|
|
261
|
+
# Add spacing between turns
|
|
262
|
+
self.text.insert(tk.END, "\n")
|
|
263
|
+
|
|
264
|
+
# Restore scroll position AFTER re-rendering
|
|
265
|
+
try:
|
|
266
|
+
self.text.yview_moveto(scroll_position)
|
|
267
|
+
except:
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
def _start_command_listener(self):
|
|
271
|
+
def server_thread():
|
|
272
|
+
if os.path.exists(self.command_socket_path):
|
|
273
|
+
os.unlink(self.command_socket_path)
|
|
274
|
+
|
|
275
|
+
server = unix_socket.socket(unix_socket.AF_UNIX, unix_socket.SOCK_STREAM)
|
|
276
|
+
server.bind(self.command_socket_path)
|
|
277
|
+
server.listen(1)
|
|
278
|
+
|
|
279
|
+
while True:
|
|
280
|
+
try:
|
|
281
|
+
conn, _ = server.accept()
|
|
282
|
+
data = conn.recv(4096)
|
|
283
|
+
if data:
|
|
284
|
+
cmd = json.loads(data.decode("utf-8"))
|
|
285
|
+
action = cmd.get("action")
|
|
286
|
+
if action == "show":
|
|
287
|
+
self.root.after(0, self._force_focus)
|
|
288
|
+
elif action == "hide":
|
|
289
|
+
self.root.after(0, self.root.withdraw)
|
|
290
|
+
elif action == "set_text":
|
|
291
|
+
text = cmd.get("text", "")
|
|
292
|
+
self.root.after(0, lambda: self._set_input_text(text))
|
|
293
|
+
conn.send(json.dumps({"status": "success"}).encode("utf-8"))
|
|
294
|
+
conn.close()
|
|
295
|
+
except Exception as e:
|
|
296
|
+
print(f"Command error: {e}")
|
|
297
|
+
|
|
298
|
+
threading.Thread(target=server_thread, daemon=True).start()
|
|
299
|
+
|
|
300
|
+
def _set_input_text(self, text):
|
|
301
|
+
"""Set input text"""
|
|
302
|
+
self.input_entry.delete(0, tk.END)
|
|
303
|
+
self.input_entry.insert(0, text)
|
|
304
|
+
|
|
305
|
+
def _force_focus(self):
|
|
306
|
+
self.root.deiconify()
|
|
307
|
+
self.root.lift()
|
|
308
|
+
self.root.attributes("-topmost", True)
|
|
309
|
+
|
|
310
|
+
try:
|
|
311
|
+
subprocess.run(
|
|
312
|
+
[
|
|
313
|
+
"osascript",
|
|
314
|
+
"-e",
|
|
315
|
+
f'tell application "System Events" to set frontmost of the first process whose unix id is {os.getpid()} to true',
|
|
316
|
+
],
|
|
317
|
+
check=False,
|
|
318
|
+
capture_output=True,
|
|
319
|
+
timeout=1,
|
|
320
|
+
)
|
|
321
|
+
except:
|
|
322
|
+
pass
|
|
323
|
+
|
|
324
|
+
self.root.focus_force()
|
|
325
|
+
self.input_entry.focus_set()
|
|
326
|
+
|
|
327
|
+
def process_messages(self):
|
|
328
|
+
"""Process messages with turn-based buffering"""
|
|
329
|
+
needs_render = False
|
|
330
|
+
|
|
331
|
+
while not self.ipc_client.message_queue.empty():
|
|
332
|
+
msg = self.ipc_client.message_queue.get()
|
|
333
|
+
msg_type = msg.get("type")
|
|
334
|
+
turn_id = msg.get("turn_id")
|
|
335
|
+
|
|
336
|
+
if msg_type == "turn_start":
|
|
337
|
+
if turn_id not in self.turns:
|
|
338
|
+
self.turns[turn_id] = {
|
|
339
|
+
"query": msg.get("data", ""),
|
|
340
|
+
"buffer": "",
|
|
341
|
+
"tools": [],
|
|
342
|
+
}
|
|
343
|
+
self.turn_order.append(turn_id)
|
|
344
|
+
needs_render = True
|
|
345
|
+
|
|
346
|
+
elif msg_type == "chunk":
|
|
347
|
+
if turn_id in self.turns:
|
|
348
|
+
chunk = msg.get("data", "")
|
|
349
|
+
self.turns[turn_id]["buffer"] += chunk
|
|
350
|
+
needs_render = True
|
|
351
|
+
|
|
352
|
+
elif msg_type == "tool_start":
|
|
353
|
+
if turn_id in self.turns:
|
|
354
|
+
tool_name = msg.get("data", "")
|
|
355
|
+
tool_num = msg.get("tool_number", 0)
|
|
356
|
+
tool_text = f"\n🛠️ #{tool_num}: {tool_name} "
|
|
357
|
+
self.turns[turn_id]["buffer"] += tool_text
|
|
358
|
+
needs_render = True
|
|
359
|
+
|
|
360
|
+
elif msg_type == "tool_end":
|
|
361
|
+
if turn_id in self.turns:
|
|
362
|
+
success = msg.get("success", False)
|
|
363
|
+
icon = "✅" if success else "❌"
|
|
364
|
+
self.turns[turn_id]["buffer"] += f"{icon}\n"
|
|
365
|
+
needs_render = True
|
|
366
|
+
|
|
367
|
+
elif msg_type == "turn_end":
|
|
368
|
+
if turn_id in self.turns:
|
|
369
|
+
self.turns[turn_id]["buffer"] += "\n"
|
|
370
|
+
needs_render = True
|
|
371
|
+
|
|
372
|
+
elif msg_type == "error":
|
|
373
|
+
error_msg = msg.get("data", "Unknown error")
|
|
374
|
+
if turn_id and turn_id in self.turns:
|
|
375
|
+
self.turns[turn_id]["buffer"] += f"\n❌ Error: {error_msg}\n"
|
|
376
|
+
needs_render = True
|
|
377
|
+
|
|
378
|
+
if needs_render:
|
|
379
|
+
self._render_all_turns()
|
|
380
|
+
|
|
381
|
+
self.root.after(50, self.process_messages)
|
|
382
|
+
|
|
383
|
+
def on_enter(self, event):
|
|
384
|
+
"""Handle Enter key in input field"""
|
|
385
|
+
query = self.input_entry.get().strip()
|
|
386
|
+
|
|
387
|
+
if not query:
|
|
388
|
+
return "break"
|
|
389
|
+
|
|
390
|
+
if not self.ipc_client.connected:
|
|
391
|
+
self._append_text("\n❌ Not connected\n", "error")
|
|
392
|
+
return "break"
|
|
393
|
+
|
|
394
|
+
try:
|
|
395
|
+
turn_id = str(uuid.uuid4())
|
|
396
|
+
self.input_entry.delete(0, tk.END)
|
|
397
|
+
|
|
398
|
+
if not self.ipc_client.send_message(query, turn_id=turn_id):
|
|
399
|
+
self._append_text("❌ Send failed\n", "error")
|
|
400
|
+
|
|
401
|
+
except Exception as e:
|
|
402
|
+
print(f"❌ Error: {e}")
|
|
403
|
+
import traceback
|
|
404
|
+
|
|
405
|
+
traceback.print_exc()
|
|
406
|
+
|
|
407
|
+
return "break"
|
|
408
|
+
|
|
409
|
+
def run(self):
|
|
410
|
+
self.root.mainloop()
|
|
411
|
+
|
|
412
|
+
def cleanup(self):
|
|
413
|
+
self.ipc_client.disconnect()
|
|
414
|
+
if os.path.exists(self.command_socket_path):
|
|
415
|
+
os.unlink(self.command_socket_path)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
if __name__ == "__main__":
|
|
419
|
+
app = MinimalAmbientInput()
|
|
420
|
+
try:
|
|
421
|
+
app.run()
|
|
422
|
+
finally:
|
|
423
|
+
app.cleanup()
|