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
devduck/__main__.py
ADDED
devduck/_version.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"__version__",
|
|
6
|
+
"__version_tuple__",
|
|
7
|
+
"version",
|
|
8
|
+
"version_tuple",
|
|
9
|
+
"__commit_id__",
|
|
10
|
+
"commit_id",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
TYPE_CHECKING = False
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import Union
|
|
17
|
+
|
|
18
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
19
|
+
COMMIT_ID = Union[str, None]
|
|
20
|
+
else:
|
|
21
|
+
VERSION_TUPLE = object
|
|
22
|
+
COMMIT_ID = object
|
|
23
|
+
|
|
24
|
+
version: str
|
|
25
|
+
__version__: str
|
|
26
|
+
__version_tuple__: VERSION_TUPLE
|
|
27
|
+
version_tuple: VERSION_TUPLE
|
|
28
|
+
commit_id: COMMIT_ID
|
|
29
|
+
__commit_id__: COMMIT_ID
|
|
30
|
+
|
|
31
|
+
__version__ = version = '0.1.1766644714'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 1, 1766644714)
|
|
33
|
+
|
|
34
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""DevDuck AgentCore Handler"""
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
from bedrock_agentcore.runtime import BedrockAgentCoreApp
|
|
7
|
+
|
|
8
|
+
# Configure for AgentCore deployment
|
|
9
|
+
os.environ["DEVDUCK_AUTO_START_SERVERS"] = "false"
|
|
10
|
+
os.environ["MODEL_PROVIDER"] = "bedrock"
|
|
11
|
+
|
|
12
|
+
from devduck import devduck
|
|
13
|
+
|
|
14
|
+
app = BedrockAgentCoreApp()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.entrypoint
|
|
18
|
+
async def invoke(payload, context):
|
|
19
|
+
"""AgentCore entrypoint - streaming by default with async generator"""
|
|
20
|
+
mode = payload.get("mode", "streaming") # streaming (default), sync, async
|
|
21
|
+
|
|
22
|
+
query = payload.get("prompt", payload.get("text", ""))
|
|
23
|
+
if not query:
|
|
24
|
+
yield {"error": "No query provided"}
|
|
25
|
+
return
|
|
26
|
+
|
|
27
|
+
print(f"Mode: {mode}, Query: {query}")
|
|
28
|
+
|
|
29
|
+
agent = devduck.agent
|
|
30
|
+
|
|
31
|
+
if mode == "sync":
|
|
32
|
+
# Sync mode - return result directly (blocking)
|
|
33
|
+
try:
|
|
34
|
+
result = agent(query)
|
|
35
|
+
yield {"statusCode": 200, "response": str(result)}
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"Error in sync: {str(e)}")
|
|
38
|
+
yield {"statusCode": 500, "error": str(e)}
|
|
39
|
+
|
|
40
|
+
elif mode == "async":
|
|
41
|
+
# Async mode - fire and forget in background thread
|
|
42
|
+
task_id = app.add_async_task("devduck_processing", payload)
|
|
43
|
+
thread = threading.Thread(
|
|
44
|
+
target=lambda: _run_in_thread(agent, query, task_id), daemon=True
|
|
45
|
+
)
|
|
46
|
+
thread.start()
|
|
47
|
+
yield {"statusCode": 200, "task_id": task_id}
|
|
48
|
+
|
|
49
|
+
else:
|
|
50
|
+
# Streaming mode (default) - stream events as they happen
|
|
51
|
+
try:
|
|
52
|
+
stream = agent.stream_async(query)
|
|
53
|
+
async for event in stream:
|
|
54
|
+
print(event)
|
|
55
|
+
yield event
|
|
56
|
+
except Exception as e:
|
|
57
|
+
print(f"Error in streaming: {str(e)}")
|
|
58
|
+
yield {"error": str(e)}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _run_in_thread(agent, query, task_id):
|
|
62
|
+
"""Run agent in background thread for async mode"""
|
|
63
|
+
try:
|
|
64
|
+
result = agent(query)
|
|
65
|
+
print(f"DevDuck result: {result}")
|
|
66
|
+
app.complete_async_task(task_id)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"Error in async thread: {str(e)}")
|
|
69
|
+
try:
|
|
70
|
+
app.complete_async_task(task_id)
|
|
71
|
+
except:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
app.run()
|
devduck/test_redduck.py
CHANGED
devduck/tools/__init__.py
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DevDuck Tools Package
|
|
3
|
+
|
|
4
|
+
This module exports all available tools for devduck.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .agentcore_agents import agentcore_agents
|
|
8
|
+
from .agentcore_config import agentcore_config
|
|
9
|
+
from .agentcore_invoke import agentcore_invoke
|
|
10
|
+
from .agentcore_logs import agentcore_logs
|
|
11
|
+
from .ambient import ambient
|
|
12
|
+
from .create_subagent import create_subagent
|
|
13
|
+
from .fetch_github_tool import fetch_github_tool
|
|
14
|
+
from .install_tools import install_tools
|
|
15
|
+
from .ipc import ipc
|
|
16
|
+
from .mcp_server import mcp_server
|
|
17
|
+
from .scraper import scraper
|
|
18
|
+
from .speech_to_speech import speech_to_speech
|
|
19
|
+
from .state_manager import state_manager
|
|
20
|
+
from .store_in_kb import store_in_kb
|
|
21
|
+
from .system_prompt import system_prompt
|
|
22
|
+
from .tcp import tcp
|
|
23
|
+
from .tray import tray
|
|
24
|
+
from .use_github import use_github
|
|
25
|
+
from .websocket import websocket
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"agentcore_agents",
|
|
29
|
+
"agentcore_config",
|
|
30
|
+
"agentcore_invoke",
|
|
31
|
+
"agentcore_logs",
|
|
32
|
+
"ambient",
|
|
33
|
+
"create_subagent",
|
|
34
|
+
"fetch_github_tool",
|
|
35
|
+
"install_tools",
|
|
36
|
+
"ipc",
|
|
37
|
+
"mcp_server",
|
|
38
|
+
"scraper",
|
|
39
|
+
"speech_to_speech",
|
|
40
|
+
"state_manager",
|
|
41
|
+
"store_in_kb",
|
|
42
|
+
"system_prompt",
|
|
43
|
+
"tcp",
|
|
44
|
+
"tray",
|
|
45
|
+
"use_github",
|
|
46
|
+
"websocket",
|
|
47
|
+
]
|
|
@@ -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()
|