abstractassistant 0.3.1__py3-none-any.whl → 0.3.3__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.
- abstractassistant/create_app_bundle.py +350 -8
- abstractassistant/ui/chat_bubble.py +15 -2
- abstractassistant/ui/history_dialog.py +18 -0
- abstractassistant/ui/qt_bubble.py +25 -12
- {abstractassistant-0.3.1.dist-info → abstractassistant-0.3.3.dist-info}/METADATA +21 -21
- {abstractassistant-0.3.1.dist-info → abstractassistant-0.3.3.dist-info}/RECORD +10 -11
- {abstractassistant-0.3.1.dist-info → abstractassistant-0.3.3.dist-info}/top_level.txt +0 -1
- setup_macos_app.py +0 -323
- {abstractassistant-0.3.1.dist-info → abstractassistant-0.3.3.dist-info}/WHEEL +0 -0
- {abstractassistant-0.3.1.dist-info → abstractassistant-0.3.3.dist-info}/entry_points.txt +0 -0
- {abstractassistant-0.3.1.dist-info → abstractassistant-0.3.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -6,21 +6,363 @@ This script can be run after installation to create a macOS app bundle
|
|
|
6
6
|
that allows launching AbstractAssistant from the Dock.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
+
import os
|
|
9
10
|
import sys
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
10
13
|
from pathlib import Path
|
|
11
14
|
|
|
15
|
+
try:
|
|
16
|
+
from PIL import Image
|
|
17
|
+
PIL_AVAILABLE = True
|
|
18
|
+
except ImportError:
|
|
19
|
+
PIL_AVAILABLE = False
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MacOSAppBundleGenerator:
|
|
23
|
+
"""Generates macOS app bundles for AbstractAssistant."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, package_dir: Path):
|
|
26
|
+
"""Initialize the app bundle generator.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
package_dir: Path to the abstractassistant package directory
|
|
30
|
+
"""
|
|
31
|
+
self.package_dir = package_dir
|
|
32
|
+
self.app_name = "AbstractAssistant"
|
|
33
|
+
self.app_bundle_path = Path("/Applications") / f"{self.app_name}.app"
|
|
34
|
+
|
|
35
|
+
def is_macos(self) -> bool:
|
|
36
|
+
"""Check if running on macOS."""
|
|
37
|
+
return sys.platform == "darwin"
|
|
38
|
+
|
|
39
|
+
def has_permissions(self) -> bool:
|
|
40
|
+
"""Check if we have permissions to write to /Applications."""
|
|
41
|
+
try:
|
|
42
|
+
test_file = Path("/Applications") / ".test_write_permission"
|
|
43
|
+
test_file.touch()
|
|
44
|
+
test_file.unlink()
|
|
45
|
+
return True
|
|
46
|
+
except (PermissionError, OSError):
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
def create_app_bundle_structure(self) -> bool:
|
|
50
|
+
"""Create the basic app bundle directory structure."""
|
|
51
|
+
try:
|
|
52
|
+
# Create main directories
|
|
53
|
+
contents_dir = self.app_bundle_path / "Contents"
|
|
54
|
+
macos_dir = contents_dir / "MacOS"
|
|
55
|
+
resources_dir = contents_dir / "Resources"
|
|
56
|
+
|
|
57
|
+
for directory in [contents_dir, macos_dir, resources_dir]:
|
|
58
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
return True
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print(f"Error creating app bundle structure: {e}")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
def generate_app_icon(self) -> bool:
|
|
66
|
+
"""Generate or preserve the app icon."""
|
|
67
|
+
try:
|
|
68
|
+
icon_path = self.app_bundle_path / "Contents" / "Resources" / "icon.png"
|
|
69
|
+
|
|
70
|
+
# Look for custom icons in multiple locations
|
|
71
|
+
custom_icon_paths = [
|
|
72
|
+
# Project directory bundle (development) - inside package directory
|
|
73
|
+
Path(self.package_dir).parent / "AbstractAssistant.app" / "Contents" / "Resources" / "icon.png",
|
|
74
|
+
# Also check if it's in the package directory itself
|
|
75
|
+
Path(self.package_dir) / "AbstractAssistant.app" / "Contents" / "Resources" / "icon.png",
|
|
76
|
+
# Any icon.png in the project root
|
|
77
|
+
Path(self.package_dir).parent / "icon.png",
|
|
78
|
+
# Any icon.png in the package directory
|
|
79
|
+
Path(self.package_dir) / "icon.png",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
custom_icon_found = False
|
|
83
|
+
|
|
84
|
+
# Try each custom icon location
|
|
85
|
+
for custom_path in custom_icon_paths:
|
|
86
|
+
if custom_path and custom_path.exists():
|
|
87
|
+
print(f"Using existing custom icon from {custom_path}")
|
|
88
|
+
shutil.copy2(str(custom_path), str(icon_path))
|
|
89
|
+
custom_icon_found = True
|
|
90
|
+
break
|
|
91
|
+
|
|
92
|
+
# If no custom icon found, try to restore from git
|
|
93
|
+
if not custom_icon_found:
|
|
94
|
+
try:
|
|
95
|
+
git_icon_path = self.package_dir.parent / "AbstractAssistant.app" / "Contents" / "Resources" / "icon.png"
|
|
96
|
+
if git_icon_path.parent.parent.parent.exists(): # Check if AbstractAssistant.app exists
|
|
97
|
+
# Try to get the icon from git
|
|
98
|
+
result = subprocess.run([
|
|
99
|
+
'git', 'show', 'HEAD:AbstractAssistant.app/Contents/Resources/icon.png'
|
|
100
|
+
], cwd=str(self.package_dir.parent), capture_output=True)
|
|
101
|
+
|
|
102
|
+
if result.returncode == 0:
|
|
103
|
+
print("Restoring custom icon from git history")
|
|
104
|
+
with open(str(icon_path), 'wb') as f:
|
|
105
|
+
f.write(result.stdout)
|
|
106
|
+
custom_icon_found = True
|
|
107
|
+
except Exception as git_error:
|
|
108
|
+
print(f"Could not restore icon from git: {git_error}")
|
|
109
|
+
|
|
110
|
+
# If still no custom icon, generate one
|
|
111
|
+
if not custom_icon_found:
|
|
112
|
+
print("Generating new icon using IconGenerator")
|
|
113
|
+
# Import the icon generator
|
|
114
|
+
sys.path.insert(0, str(self.package_dir))
|
|
115
|
+
from abstractassistant.utils.icon_generator import IconGenerator
|
|
116
|
+
|
|
117
|
+
# Generate high-resolution icon
|
|
118
|
+
generator = IconGenerator(size=512)
|
|
119
|
+
icon = generator.create_app_icon('blue', animated=False)
|
|
120
|
+
|
|
121
|
+
# Save as PNG
|
|
122
|
+
icon.save(str(icon_path))
|
|
123
|
+
|
|
124
|
+
# Create ICNS file
|
|
125
|
+
return self._create_icns_file(icon_path)
|
|
126
|
+
|
|
127
|
+
except Exception as e:
|
|
128
|
+
print(f"Error generating app icon: {e}")
|
|
129
|
+
return False
|
|
130
|
+
|
|
131
|
+
def _create_icns_file(self, png_path: Path) -> bool:
|
|
132
|
+
"""Create ICNS file from PNG using macOS iconutil."""
|
|
133
|
+
try:
|
|
134
|
+
# Create iconset directory
|
|
135
|
+
iconset_dir = png_path.parent / "temp_icons.iconset"
|
|
136
|
+
iconset_dir.mkdir(exist_ok=True)
|
|
137
|
+
|
|
138
|
+
# Load the PNG and create different sizes
|
|
139
|
+
icon = Image.open(png_path)
|
|
140
|
+
sizes = [
|
|
141
|
+
(16, 'icon_16x16.png'),
|
|
142
|
+
(32, 'icon_16x16@2x.png'),
|
|
143
|
+
(32, 'icon_32x32.png'),
|
|
144
|
+
(64, 'icon_32x32@2x.png'),
|
|
145
|
+
(128, 'icon_128x128.png'),
|
|
146
|
+
(256, 'icon_128x128@2x.png'),
|
|
147
|
+
(256, 'icon_256x256.png'),
|
|
148
|
+
(512, 'icon_256x256@2x.png'),
|
|
149
|
+
(512, 'icon_512x512.png'),
|
|
150
|
+
(1024, 'icon_512x512@2x.png')
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
for size, filename in sizes:
|
|
154
|
+
resized = icon.resize((size, size), Image.Resampling.LANCZOS)
|
|
155
|
+
resized.save(iconset_dir / filename)
|
|
156
|
+
|
|
157
|
+
# Convert to ICNS
|
|
158
|
+
icns_path = png_path.parent / "icon.icns"
|
|
159
|
+
result = subprocess.run([
|
|
160
|
+
'iconutil', '-c', 'icns', str(iconset_dir),
|
|
161
|
+
'-o', str(icns_path)
|
|
162
|
+
], capture_output=True, text=True)
|
|
163
|
+
|
|
164
|
+
# Clean up
|
|
165
|
+
shutil.rmtree(iconset_dir)
|
|
166
|
+
|
|
167
|
+
return result.returncode == 0
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"Error creating ICNS file: {e}")
|
|
171
|
+
return False
|
|
172
|
+
|
|
173
|
+
def create_info_plist(self) -> bool:
|
|
174
|
+
"""Create the Info.plist file."""
|
|
175
|
+
try:
|
|
176
|
+
plist_content = '''<?xml version="1.0" encoding="UTF-8"?>
|
|
177
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
178
|
+
<plist version="1.0">
|
|
179
|
+
<dict>
|
|
180
|
+
<key>CFBundleExecutable</key>
|
|
181
|
+
<string>AbstractAssistant</string>
|
|
182
|
+
<key>CFBundleIdentifier</key>
|
|
183
|
+
<string>ai.abstractcore.abstractassistant</string>
|
|
184
|
+
<key>CFBundleName</key>
|
|
185
|
+
<string>AbstractAssistant</string>
|
|
186
|
+
<key>CFBundleDisplayName</key>
|
|
187
|
+
<string>AbstractAssistant</string>
|
|
188
|
+
<key>CFBundleVersion</key>
|
|
189
|
+
<string>0.3.2</string>
|
|
190
|
+
<key>CFBundleShortVersionString</key>
|
|
191
|
+
<string>0.3.2</string>
|
|
192
|
+
<key>CFBundlePackageType</key>
|
|
193
|
+
<string>APPL</string>
|
|
194
|
+
<key>CFBundleSignature</key>
|
|
195
|
+
<string>????</string>
|
|
196
|
+
<key>CFBundleIconFile</key>
|
|
197
|
+
<string>icon.icns</string>
|
|
198
|
+
<key>LSMinimumSystemVersion</key>
|
|
199
|
+
<string>10.15</string>
|
|
200
|
+
<key>NSHighResolutionCapable</key>
|
|
201
|
+
<true/>
|
|
202
|
+
<key>NSRequiresAquaSystemAppearance</key>
|
|
203
|
+
<false/>
|
|
204
|
+
<key>LSUIElement</key>
|
|
205
|
+
<true/>
|
|
206
|
+
<key>NSAppleScriptEnabled</key>
|
|
207
|
+
<false/>
|
|
208
|
+
<key>CFBundleDocumentTypes</key>
|
|
209
|
+
<array/>
|
|
210
|
+
<key>NSPrincipalClass</key>
|
|
211
|
+
<string>NSApplication</string>
|
|
212
|
+
</dict>
|
|
213
|
+
</plist>'''
|
|
214
|
+
|
|
215
|
+
plist_path = self.app_bundle_path / "Contents" / "Info.plist"
|
|
216
|
+
plist_path.write_text(plist_content)
|
|
217
|
+
return True
|
|
218
|
+
|
|
219
|
+
except Exception as e:
|
|
220
|
+
print(f"Error creating Info.plist: {e}")
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
def create_launch_script(self) -> bool:
|
|
224
|
+
"""Create the executable launch script."""
|
|
225
|
+
try:
|
|
226
|
+
script_content = '''#!/bin/bash
|
|
227
|
+
|
|
228
|
+
# AbstractAssistant macOS App Launcher
|
|
229
|
+
# This script launches the AbstractAssistant application
|
|
230
|
+
|
|
231
|
+
# Set up environment paths for GUI launch (common locations)
|
|
232
|
+
export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:$PATH"
|
|
233
|
+
|
|
234
|
+
# Add user-specific Python paths if they exist
|
|
235
|
+
if [ -d "$HOME/.pyenv/shims" ]; then
|
|
236
|
+
export PATH="$HOME/.pyenv/shims:$PATH"
|
|
237
|
+
fi
|
|
238
|
+
|
|
239
|
+
if [ -d "$HOME/.local/bin" ]; then
|
|
240
|
+
export PATH="$HOME/.local/bin:$PATH"
|
|
241
|
+
fi
|
|
242
|
+
|
|
243
|
+
if [ -d "/opt/anaconda3/bin" ]; then
|
|
244
|
+
export PATH="/opt/anaconda3/bin:$PATH"
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
if [ -d "$HOME/anaconda3/bin" ]; then
|
|
248
|
+
export PATH="$HOME/anaconda3/bin:$PATH"
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
# Function to find Python with abstractassistant installed
|
|
252
|
+
find_python_with_abstractassistant() {
|
|
253
|
+
# First try pyenv's active Python (most reliable)
|
|
254
|
+
if [ -x "$HOME/.pyenv/shims/python3" ]; then
|
|
255
|
+
if "$HOME/.pyenv/shims/python3" -c "import abstractassistant" 2>/dev/null; then
|
|
256
|
+
echo "$HOME/.pyenv/shims/python3"
|
|
257
|
+
return 0
|
|
258
|
+
fi
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
# Try PATH-based search (respects current environment)
|
|
262
|
+
for python_cmd in python3 python python3.13 python3.12 python3.11 python3.10 python3.9; do
|
|
263
|
+
if command -v "$python_cmd" >/dev/null 2>&1; then
|
|
264
|
+
if "$python_cmd" -c "import abstractassistant" 2>/dev/null; then
|
|
265
|
+
echo "$python_cmd"
|
|
266
|
+
return 0
|
|
267
|
+
fi
|
|
268
|
+
fi
|
|
269
|
+
done
|
|
270
|
+
|
|
271
|
+
# Try specific pyenv versions (sorted by version number, newest first)
|
|
272
|
+
for version_dir in $(ls -1v "$HOME/.pyenv/versions/" 2>/dev/null | sort -V -r); do
|
|
273
|
+
py="$HOME/.pyenv/versions/$version_dir/bin/python3"
|
|
274
|
+
if [ -x "$py" ] && "$py" -c "import abstractassistant" 2>/dev/null; then
|
|
275
|
+
echo "$py"
|
|
276
|
+
return 0
|
|
277
|
+
fi
|
|
278
|
+
done
|
|
279
|
+
|
|
280
|
+
# Try other common locations
|
|
281
|
+
for python_path in \\
|
|
282
|
+
"/usr/local/bin/python3" \\
|
|
283
|
+
"/opt/homebrew/bin/python3" \\
|
|
284
|
+
"/usr/bin/python3" \\
|
|
285
|
+
"/opt/anaconda3/bin/python" \\
|
|
286
|
+
"$HOME/anaconda3/bin/python" \\
|
|
287
|
+
"/usr/local/anaconda3/bin/python"; do
|
|
288
|
+
|
|
289
|
+
if [ -x "$python_path" ] && "$python_path" -c "import abstractassistant" 2>/dev/null; then
|
|
290
|
+
echo "$python_path"
|
|
291
|
+
return 0
|
|
292
|
+
fi
|
|
293
|
+
done
|
|
294
|
+
|
|
295
|
+
return 1
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
# Find Python with AbstractAssistant
|
|
299
|
+
PYTHON_EXEC=$(find_python_with_abstractassistant)
|
|
300
|
+
|
|
301
|
+
if [ -z "$PYTHON_EXEC" ]; then
|
|
302
|
+
osascript -e 'display dialog "AbstractAssistant not found in any Python installation.\\n\\nPlease install it with:\\npip install abstractassistant\\n\\nOr run the create-app-bundle command after installation." with title "AbstractAssistant" buttons {"OK"} default button "OK" with icon caution'
|
|
303
|
+
exit 1
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
# Change to a neutral directory to avoid importing development versions
|
|
307
|
+
cd /tmp
|
|
308
|
+
|
|
309
|
+
# Launch the assistant
|
|
310
|
+
exec "$PYTHON_EXEC" -m abstractassistant.cli "$@"'''
|
|
311
|
+
|
|
312
|
+
script_path = self.app_bundle_path / "Contents" / "MacOS" / "AbstractAssistant"
|
|
313
|
+
script_path.write_text(script_content)
|
|
314
|
+
|
|
315
|
+
# Make executable
|
|
316
|
+
os.chmod(script_path, 0o755)
|
|
317
|
+
return True
|
|
318
|
+
|
|
319
|
+
except Exception as e:
|
|
320
|
+
print(f"Error creating launch script: {e}")
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
def generate_app_bundle(self) -> bool:
|
|
324
|
+
"""Generate the complete macOS app bundle."""
|
|
325
|
+
if not self.is_macos():
|
|
326
|
+
print("macOS app bundle generation is only available on macOS")
|
|
327
|
+
return False
|
|
328
|
+
|
|
329
|
+
if not self.has_permissions():
|
|
330
|
+
print("Insufficient permissions to create app bundle in /Applications")
|
|
331
|
+
print("Please run with sudo or manually copy the app bundle")
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
print("Creating macOS app bundle...")
|
|
335
|
+
|
|
336
|
+
# Remove existing bundle if it exists
|
|
337
|
+
if self.app_bundle_path.exists():
|
|
338
|
+
shutil.rmtree(self.app_bundle_path)
|
|
339
|
+
|
|
340
|
+
# Create bundle structure
|
|
341
|
+
if not self.create_app_bundle_structure():
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
# Generate icon
|
|
345
|
+
if not self.generate_app_icon():
|
|
346
|
+
return False
|
|
347
|
+
|
|
348
|
+
# Create Info.plist
|
|
349
|
+
if not self.create_info_plist():
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
# Create launch script
|
|
353
|
+
if not self.create_launch_script():
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
print(f"✅ macOS app bundle created successfully!")
|
|
357
|
+
print(f" Location: {self.app_bundle_path}")
|
|
358
|
+
print(f" You can now launch AbstractAssistant from the Dock!")
|
|
359
|
+
|
|
360
|
+
return True
|
|
361
|
+
|
|
12
362
|
|
|
13
363
|
def main():
|
|
14
364
|
"""Create macOS app bundle for AbstractAssistant."""
|
|
15
365
|
try:
|
|
16
|
-
# Import the app bundle generator
|
|
17
|
-
try:
|
|
18
|
-
import setup_macos_app
|
|
19
|
-
MacOSAppBundleGenerator = setup_macos_app.MacOSAppBundleGenerator
|
|
20
|
-
except ImportError:
|
|
21
|
-
# Fallback: try importing from abstractassistant package
|
|
22
|
-
from abstractassistant.setup_macos_app import MacOSAppBundleGenerator
|
|
23
|
-
|
|
24
366
|
# Find the package directory
|
|
25
367
|
import abstractassistant
|
|
26
368
|
package_dir = Path(abstractassistant.__file__).parent
|
|
@@ -121,8 +121,10 @@ class ChatBubble:
|
|
|
121
121
|
)
|
|
122
122
|
self.text_input.pack(fill="both", expand=True)
|
|
123
123
|
|
|
124
|
-
# Bind
|
|
125
|
-
|
|
124
|
+
# Bind keyboard shortcuts for message sending
|
|
125
|
+
# Enter = send message, Shift+Enter = new line
|
|
126
|
+
self.text_input.bind("<Return>", self._handle_enter_key)
|
|
127
|
+
self.text_input.bind("<KP_Enter>", self._handle_enter_key) # Numpad Enter
|
|
126
128
|
|
|
127
129
|
# Controls frame
|
|
128
130
|
controls_frame = ctk.CTkFrame(main_frame, fg_color="transparent")
|
|
@@ -271,6 +273,17 @@ class ChatBubble:
|
|
|
271
273
|
text_color=status_colors.get(status, "gray")
|
|
272
274
|
)
|
|
273
275
|
|
|
276
|
+
def _handle_enter_key(self, event):
|
|
277
|
+
"""Handle Enter key press in text input."""
|
|
278
|
+
# Check if Shift is held down
|
|
279
|
+
if event.state & 0x1: # Shift modifier
|
|
280
|
+
# Shift+Enter: Allow default behavior (new line)
|
|
281
|
+
return None
|
|
282
|
+
else:
|
|
283
|
+
# Plain Enter: Send message
|
|
284
|
+
self._send_message()
|
|
285
|
+
return "break" # Prevent default behavior
|
|
286
|
+
|
|
274
287
|
def _send_message(self):
|
|
275
288
|
"""Send the current message."""
|
|
276
289
|
if self.is_sending or self.text_input is None:
|
|
@@ -359,6 +359,24 @@ class iPhoneMessagesDialog:
|
|
|
359
359
|
layout.addStretch()
|
|
360
360
|
|
|
361
361
|
bubble_layout.addWidget(content_label)
|
|
362
|
+
|
|
363
|
+
# Add file attachment indicator if files were attached to this message
|
|
364
|
+
attached_files = msg.get('attached_files', [])
|
|
365
|
+
if attached_files:
|
|
366
|
+
file_indicator = QLabel(f"📎 {len(attached_files)} file{'s' if len(attached_files) > 1 else ''}")
|
|
367
|
+
file_indicator.setStyleSheet("""
|
|
368
|
+
QLabel {
|
|
369
|
+
background: transparent;
|
|
370
|
+
color: rgba(255, 255, 255, 0.7);
|
|
371
|
+
font-size: 11px;
|
|
372
|
+
font-weight: 500;
|
|
373
|
+
font-family: "Helvetica Neue", "Helvetica", Arial, sans-serif;
|
|
374
|
+
padding: 2px 0px;
|
|
375
|
+
margin: 0px;
|
|
376
|
+
}
|
|
377
|
+
""")
|
|
378
|
+
bubble_layout.addWidget(file_indicator)
|
|
379
|
+
|
|
362
380
|
main_layout.addWidget(container)
|
|
363
381
|
|
|
364
382
|
# Add timestamp below bubble (iPhone style)
|
|
@@ -289,6 +289,9 @@ class QtChatBubble(QWidget):
|
|
|
289
289
|
# Attached files for media handling (AbstractCore 2.4.5+)
|
|
290
290
|
self.attached_files: List[str] = []
|
|
291
291
|
|
|
292
|
+
# Track file attachments per message for history display
|
|
293
|
+
self.message_file_attachments: Dict[int, List[str]] = {}
|
|
294
|
+
|
|
292
295
|
# Initialize new manager classes
|
|
293
296
|
self.provider_manager = None
|
|
294
297
|
self.tts_state_manager = None
|
|
@@ -1067,13 +1070,15 @@ class QtChatBubble(QWidget):
|
|
|
1067
1070
|
|
|
1068
1071
|
# Check for Enter/Return key
|
|
1069
1072
|
if event.key() == Qt.Key.Key_Return or event.key() == Qt.Key.Key_Enter:
|
|
1070
|
-
# Shift+Enter
|
|
1071
|
-
if
|
|
1072
|
-
|
|
1073
|
-
|
|
1073
|
+
# Shift+Enter should add a new line
|
|
1074
|
+
if event.modifiers() & Qt.KeyboardModifier.ShiftModifier:
|
|
1075
|
+
# Allow default behavior (new line)
|
|
1076
|
+
QTextEdit.keyPressEvent(self.input_text, event)
|
|
1077
|
+
return
|
|
1078
|
+
# Plain Enter should send message
|
|
1079
|
+
else:
|
|
1074
1080
|
self.send_message()
|
|
1075
1081
|
return
|
|
1076
|
-
# Plain Enter should add a new line (default behavior)
|
|
1077
1082
|
|
|
1078
1083
|
# Call original keyPressEvent for all other keys
|
|
1079
1084
|
QTextEdit.keyPressEvent(self.input_text, event)
|
|
@@ -1255,12 +1260,18 @@ class QtChatBubble(QWidget):
|
|
|
1255
1260
|
# 1. Clear input immediately
|
|
1256
1261
|
self.input_text.clear()
|
|
1257
1262
|
|
|
1258
|
-
# 2. Capture attached files
|
|
1263
|
+
# 2. Capture attached files for sending (but keep them attached)
|
|
1259
1264
|
media_files = self.attached_files.copy()
|
|
1265
|
+
|
|
1266
|
+
# 3. Store file attachments for this message in our tracking dict
|
|
1267
|
+
# We'll use the message count as a simple key
|
|
1268
|
+
if media_files:
|
|
1269
|
+
message_index = len(self.message_history) # Current message index before adding
|
|
1270
|
+
self.message_file_attachments[message_index] = media_files.copy()
|
|
1271
|
+
if self.debug:
|
|
1272
|
+
print(f"📎 Storing {len(media_files)} file(s) for message index {message_index}")
|
|
1260
1273
|
|
|
1261
|
-
#
|
|
1262
|
-
self.attached_files.clear()
|
|
1263
|
-
self.update_attached_files_display()
|
|
1274
|
+
# Note: We no longer clear attached_files here - they persist for reuse
|
|
1264
1275
|
|
|
1265
1276
|
# 4. Update UI for sending state
|
|
1266
1277
|
self.send_button.setEnabled(False)
|
|
@@ -1953,11 +1964,12 @@ class QtChatBubble(QWidget):
|
|
|
1953
1964
|
|
|
1954
1965
|
# Clear attached files as part of session clearing
|
|
1955
1966
|
self.attached_files.clear()
|
|
1967
|
+
self.message_file_attachments.clear()
|
|
1956
1968
|
self.update_attached_files_display()
|
|
1957
1969
|
|
|
1958
1970
|
if self.debug:
|
|
1959
1971
|
if self.debug:
|
|
1960
|
-
print("🧹 Session cleared (including attached files)")
|
|
1972
|
+
print("🧹 Session cleared (including attached files and file tracking)")
|
|
1961
1973
|
|
|
1962
1974
|
def load_session(self):
|
|
1963
1975
|
"""Load a session using AbstractCore via LLMManager."""
|
|
@@ -2090,7 +2102,7 @@ class QtChatBubble(QWidget):
|
|
|
2090
2102
|
|
|
2091
2103
|
# Convert AbstractCore messages to our format
|
|
2092
2104
|
self.message_history = []
|
|
2093
|
-
for msg in session_messages:
|
|
2105
|
+
for i, msg in enumerate(session_messages):
|
|
2094
2106
|
# Skip system messages
|
|
2095
2107
|
if hasattr(msg, 'role') and msg.role == 'system':
|
|
2096
2108
|
continue
|
|
@@ -2100,7 +2112,8 @@ class QtChatBubble(QWidget):
|
|
|
2100
2112
|
'type': getattr(msg, 'role', 'unknown'),
|
|
2101
2113
|
'content': getattr(msg, 'content', str(msg)),
|
|
2102
2114
|
'provider': self.current_provider,
|
|
2103
|
-
'model': self.current_model
|
|
2115
|
+
'model': self.current_model,
|
|
2116
|
+
'attached_files': self.message_file_attachments.get(len(self.message_history), [])
|
|
2104
2117
|
}
|
|
2105
2118
|
self.message_history.append(message)
|
|
2106
2119
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: abstractassistant
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: A sleek (macOS) system tray application providing instant access to LLMs
|
|
5
5
|
Author-email: Laurent-Philippe Albou <contact@abstractcore.ai>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -211,26 +211,26 @@ assistant --debug
|
|
|
211
211
|
|
|
212
212
|
```
|
|
213
213
|
abstractassistant/
|
|
214
|
-
├── pyproject.toml
|
|
215
|
-
├── requirements.txt
|
|
216
|
-
├── config.toml
|
|
217
|
-
├── abstractassistant/
|
|
218
|
-
│ ├── cli.py
|
|
219
|
-
│ ├── app.py
|
|
220
|
-
│ ├── config.py
|
|
221
|
-
│ ├── core/
|
|
222
|
-
│ │ ├── llm_manager.py
|
|
223
|
-
│ │ └── tts_manager.py
|
|
224
|
-
│ ├── ui/
|
|
225
|
-
│ │ ├── qt_bubble.py
|
|
226
|
-
│ │ └── toast_window.py
|
|
227
|
-
│ └── utils/
|
|
228
|
-
│ ├── icon_generator.py
|
|
229
|
-
│ └── markdown_renderer.py
|
|
230
|
-
└── docs/
|
|
231
|
-
├──
|
|
232
|
-
├──
|
|
233
|
-
└──
|
|
214
|
+
├── pyproject.toml # Package configuration
|
|
215
|
+
├── requirements.txt # Dependencies
|
|
216
|
+
├── config.toml # Default configuration
|
|
217
|
+
├── abstractassistant/ # Main package
|
|
218
|
+
│ ├── cli.py # CLI entry point
|
|
219
|
+
│ ├── app.py # Main application
|
|
220
|
+
│ ├── config.py # Configuration management
|
|
221
|
+
│ ├── core/ # Business logic
|
|
222
|
+
│ │ ├── llm_manager.py # LLM provider management
|
|
223
|
+
│ │ └── tts_manager.py # Voice/TTS integration
|
|
224
|
+
│ ├── ui/ # User interface
|
|
225
|
+
│ │ ├── qt_bubble.py # Main Qt chat interface
|
|
226
|
+
│ │ └── toast_window.py # Notification system
|
|
227
|
+
│ └── utils/ # Utilities
|
|
228
|
+
│ ├── icon_generator.py # Dynamic icon creation
|
|
229
|
+
│ └── markdown_renderer.py # Markdown processing
|
|
230
|
+
└── docs/ # Documentation
|
|
231
|
+
├── architecture.md # Technical documentation
|
|
232
|
+
├── installation.md # Installation guide
|
|
233
|
+
└── getting-started.md # Usage guide
|
|
234
234
|
```
|
|
235
235
|
|
|
236
236
|
## 🌟 Why AbstractAssistant?
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
setup_macos_app.py,sha256=9dIPr9TipjtgdIhd0MnR2syRNoFyBVMnRsWDW0UCT3A,10736
|
|
2
1
|
abstractassistant/__init__.py,sha256=homfqMDh6sX2nBROtk6-y72jnrStPph8gEOeT0OjKyU,35
|
|
3
2
|
abstractassistant/app.py,sha256=yGFszbaqja_Y1ejSMcVYIcq8f1qdeZpVTb032geI-ZE,40374
|
|
4
3
|
abstractassistant/cli.py,sha256=SQPxQCLjX-LOlhSEvG302D0AOyxlxo5QM2imxr9wxmc,4385
|
|
5
4
|
abstractassistant/config.py,sha256=KodfPYTpHtavJyne-h-B-r3kbEt1uusSY8GknGLtDL8,5809
|
|
6
|
-
abstractassistant/create_app_bundle.py,sha256=
|
|
5
|
+
abstractassistant/create_app_bundle.py,sha256=lJJsdnjl-WSdQVn8uFp7c_3cJkx8liZbK2C7elOCG1A,14215
|
|
7
6
|
abstractassistant/web_server.py,sha256=_pqMzy13qfim9BMBqQJQifWyX7UQXFD_sZeiu4ZBt40,12816
|
|
8
7
|
abstractassistant/core/__init__.py,sha256=TETStgToTe7QSsCZgRHDk2oSErlLJoeGN0sFg4Yx2_c,15
|
|
9
8
|
abstractassistant/core/llm_manager.py,sha256=hJun-nDfRv9zxv_3tfrHAmVYSYT96E-0zDJB2TiaSeQ,19226
|
|
10
9
|
abstractassistant/core/tts_manager.py,sha256=Cxh302EgIycwkWxe7XntmLW-j_WusbJOYRCs3Jms3CU,9892
|
|
11
10
|
abstractassistant/ui/__init__.py,sha256=aRNE2pS50nFAX6y--rSGMNYwhz905g14gRd6g4BolYU,13
|
|
12
|
-
abstractassistant/ui/chat_bubble.py,sha256=
|
|
13
|
-
abstractassistant/ui/history_dialog.py,sha256=
|
|
11
|
+
abstractassistant/ui/chat_bubble.py,sha256=bY48b4IeQzOrRN2_sJ5OazhZcJ8IMaBM6R3EexvU30Q,11885
|
|
12
|
+
abstractassistant/ui/history_dialog.py,sha256=949l8fVgRpUfRGcoOVRutERVXrShYgpKkFJCxo2JG-4,19323
|
|
14
13
|
abstractassistant/ui/provider_manager.py,sha256=9IM-BxIs6lUlk6cDCBi7oZFMXmn4CFMlxh0s-_vhzXY,8403
|
|
15
|
-
abstractassistant/ui/qt_bubble.py,sha256
|
|
14
|
+
abstractassistant/ui/qt_bubble.py,sha256=ujJtoDuVuXthxfXsvHX-YbqV4GuUgNp2FGy_wT3t4UY,96990
|
|
16
15
|
abstractassistant/ui/toast_manager.py,sha256=1aU4DPo-J45bC61gTEctHq98ZrHIFxRfZa_9Q8KF588,13721
|
|
17
16
|
abstractassistant/ui/toast_window.py,sha256=BRSwEBlaND5LLipn1HOX0ISWxVH-zOHsYplFkiPaj_g,21727
|
|
18
17
|
abstractassistant/ui/tts_state_manager.py,sha256=UF_zrfl9wf0hNHBGxevcoKxW5Dh7zXibUSVoSSjGP4o,10565
|
|
@@ -20,9 +19,9 @@ abstractassistant/ui/ui_styles.py,sha256=FvE2CVUbHmHu1PKVTBBGyhbt781qh4WjLMrHvil
|
|
|
20
19
|
abstractassistant/utils/__init__.py,sha256=7Q3BxyXETkt3tm5trhuLTyL8PoECOK0QiK-0KUVAR2Q,16
|
|
21
20
|
abstractassistant/utils/icon_generator.py,sha256=SWPgi1V6_8544Zbc2vAfFXAy15H35neyUGCYt2eKoic,16475
|
|
22
21
|
abstractassistant/utils/markdown_renderer.py,sha256=u5tVIhulSwRYADiqJcZNoHhU8e6pJVgzrwZRd61Bov0,12585
|
|
23
|
-
abstractassistant-0.3.
|
|
24
|
-
abstractassistant-0.3.
|
|
25
|
-
abstractassistant-0.3.
|
|
26
|
-
abstractassistant-0.3.
|
|
27
|
-
abstractassistant-0.3.
|
|
28
|
-
abstractassistant-0.3.
|
|
22
|
+
abstractassistant-0.3.3.dist-info/licenses/LICENSE,sha256=QUjFNAE-0yOkW9-Rle2axkpkt9H7xiZ2VbN-VeONhxc,1106
|
|
23
|
+
abstractassistant-0.3.3.dist-info/METADATA,sha256=gHXdK078vCVh3vMAdjAxEd32JPShNpV3DCkNWLlUjZ0,11564
|
|
24
|
+
abstractassistant-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
25
|
+
abstractassistant-0.3.3.dist-info/entry_points.txt,sha256=MIzeCh0XG6MbhIzBHtkdEjmjxYBsQrGFevq8Y1L8Jkc,118
|
|
26
|
+
abstractassistant-0.3.3.dist-info/top_level.txt,sha256=qZc_LQH3CBxLq2P4B1aHayzkj8hn0euR31edkXQVzDA,18
|
|
27
|
+
abstractassistant-0.3.3.dist-info/RECORD,,
|
setup_macos_app.py
DELETED
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
macOS App Bundle Generator for AbstractAssistant.
|
|
4
|
-
|
|
5
|
-
Creates a native macOS .app bundle with Dock integration and system tray support.
|
|
6
|
-
Usage: create-app-bundle (after pip install abstractassistant)
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import os
|
|
10
|
-
import sys
|
|
11
|
-
import shutil
|
|
12
|
-
import subprocess
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
try:
|
|
16
|
-
from PIL import Image
|
|
17
|
-
PIL_AVAILABLE = True
|
|
18
|
-
except ImportError:
|
|
19
|
-
PIL_AVAILABLE = False
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class MacOSAppBundleGenerator:
|
|
23
|
-
"""Generates macOS app bundles for AbstractAssistant."""
|
|
24
|
-
|
|
25
|
-
def __init__(self, package_dir: Path):
|
|
26
|
-
"""Initialize the app bundle generator.
|
|
27
|
-
|
|
28
|
-
Args:
|
|
29
|
-
package_dir: Path to the abstractassistant package directory
|
|
30
|
-
"""
|
|
31
|
-
self.package_dir = package_dir
|
|
32
|
-
self.app_name = "AbstractAssistant"
|
|
33
|
-
self.app_bundle_path = Path("/Applications") / f"{self.app_name}.app"
|
|
34
|
-
|
|
35
|
-
def is_macos(self) -> bool:
|
|
36
|
-
"""Check if running on macOS."""
|
|
37
|
-
return sys.platform == "darwin"
|
|
38
|
-
|
|
39
|
-
def has_permissions(self) -> bool:
|
|
40
|
-
"""Check if we have permissions to write to /Applications."""
|
|
41
|
-
try:
|
|
42
|
-
test_file = Path("/Applications") / ".test_write_permission"
|
|
43
|
-
test_file.touch()
|
|
44
|
-
test_file.unlink()
|
|
45
|
-
return True
|
|
46
|
-
except (PermissionError, OSError):
|
|
47
|
-
return False
|
|
48
|
-
|
|
49
|
-
def create_app_bundle_structure(self) -> bool:
|
|
50
|
-
"""Create the basic app bundle directory structure."""
|
|
51
|
-
try:
|
|
52
|
-
# Create main directories
|
|
53
|
-
contents_dir = self.app_bundle_path / "Contents"
|
|
54
|
-
macos_dir = contents_dir / "MacOS"
|
|
55
|
-
resources_dir = contents_dir / "Resources"
|
|
56
|
-
|
|
57
|
-
for directory in [contents_dir, macos_dir, resources_dir]:
|
|
58
|
-
directory.mkdir(parents=True, exist_ok=True)
|
|
59
|
-
|
|
60
|
-
return True
|
|
61
|
-
except Exception as e:
|
|
62
|
-
print(f"Error creating app bundle structure: {e}")
|
|
63
|
-
return False
|
|
64
|
-
|
|
65
|
-
def generate_app_icon(self) -> bool:
|
|
66
|
-
"""Generate the app icon using the existing icon generator."""
|
|
67
|
-
try:
|
|
68
|
-
# Import the icon generator
|
|
69
|
-
sys.path.insert(0, str(self.package_dir))
|
|
70
|
-
from abstractassistant.utils.icon_generator import IconGenerator
|
|
71
|
-
|
|
72
|
-
# Generate high-resolution icon
|
|
73
|
-
generator = IconGenerator(size=512)
|
|
74
|
-
icon = generator.create_app_icon('blue', animated=False)
|
|
75
|
-
|
|
76
|
-
# Save as PNG
|
|
77
|
-
icon_path = self.app_bundle_path / "Contents" / "Resources" / "icon.png"
|
|
78
|
-
icon.save(str(icon_path))
|
|
79
|
-
|
|
80
|
-
# Create ICNS file
|
|
81
|
-
return self._create_icns_file(icon_path)
|
|
82
|
-
|
|
83
|
-
except Exception as e:
|
|
84
|
-
print(f"Error generating app icon: {e}")
|
|
85
|
-
return False
|
|
86
|
-
|
|
87
|
-
def _create_icns_file(self, png_path: Path) -> bool:
|
|
88
|
-
"""Create ICNS file from PNG using macOS iconutil."""
|
|
89
|
-
try:
|
|
90
|
-
# Create iconset directory
|
|
91
|
-
iconset_dir = png_path.parent / "temp_icons.iconset"
|
|
92
|
-
iconset_dir.mkdir(exist_ok=True)
|
|
93
|
-
|
|
94
|
-
# Load the PNG and create different sizes
|
|
95
|
-
icon = Image.open(png_path)
|
|
96
|
-
sizes = [
|
|
97
|
-
(16, 'icon_16x16.png'),
|
|
98
|
-
(32, 'icon_16x16@2x.png'),
|
|
99
|
-
(32, 'icon_32x32.png'),
|
|
100
|
-
(64, 'icon_32x32@2x.png'),
|
|
101
|
-
(128, 'icon_128x128.png'),
|
|
102
|
-
(256, 'icon_128x128@2x.png'),
|
|
103
|
-
(256, 'icon_256x256.png'),
|
|
104
|
-
(512, 'icon_256x256@2x.png'),
|
|
105
|
-
(512, 'icon_512x512.png'),
|
|
106
|
-
(1024, 'icon_512x512@2x.png')
|
|
107
|
-
]
|
|
108
|
-
|
|
109
|
-
for size, filename in sizes:
|
|
110
|
-
resized = icon.resize((size, size), Image.Resampling.LANCZOS)
|
|
111
|
-
resized.save(iconset_dir / filename)
|
|
112
|
-
|
|
113
|
-
# Convert to ICNS
|
|
114
|
-
icns_path = png_path.parent / "icon.icns"
|
|
115
|
-
result = subprocess.run([
|
|
116
|
-
'iconutil', '-c', 'icns', str(iconset_dir),
|
|
117
|
-
'-o', str(icns_path)
|
|
118
|
-
], capture_output=True, text=True)
|
|
119
|
-
|
|
120
|
-
# Clean up
|
|
121
|
-
shutil.rmtree(iconset_dir)
|
|
122
|
-
|
|
123
|
-
return result.returncode == 0
|
|
124
|
-
|
|
125
|
-
except Exception as e:
|
|
126
|
-
print(f"Error creating ICNS file: {e}")
|
|
127
|
-
return False
|
|
128
|
-
|
|
129
|
-
def create_info_plist(self) -> bool:
|
|
130
|
-
"""Create the Info.plist file."""
|
|
131
|
-
try:
|
|
132
|
-
plist_content = '''<?xml version="1.0" encoding="UTF-8"?>
|
|
133
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
134
|
-
<plist version="1.0">
|
|
135
|
-
<dict>
|
|
136
|
-
<key>CFBundleExecutable</key>
|
|
137
|
-
<string>AbstractAssistant</string>
|
|
138
|
-
<key>CFBundleIdentifier</key>
|
|
139
|
-
<string>ai.abstractcore.abstractassistant</string>
|
|
140
|
-
<key>CFBundleName</key>
|
|
141
|
-
<string>AbstractAssistant</string>
|
|
142
|
-
<key>CFBundleDisplayName</key>
|
|
143
|
-
<string>AbstractAssistant</string>
|
|
144
|
-
<key>CFBundleVersion</key>
|
|
145
|
-
<string>0.2.8</string>
|
|
146
|
-
<key>CFBundleShortVersionString</key>
|
|
147
|
-
<string>0.2.8</string>
|
|
148
|
-
<key>CFBundlePackageType</key>
|
|
149
|
-
<string>APPL</string>
|
|
150
|
-
<key>CFBundleSignature</key>
|
|
151
|
-
<string>????</string>
|
|
152
|
-
<key>CFBundleIconFile</key>
|
|
153
|
-
<string>icon.icns</string>
|
|
154
|
-
<key>LSMinimumSystemVersion</key>
|
|
155
|
-
<string>10.15</string>
|
|
156
|
-
<key>NSHighResolutionCapable</key>
|
|
157
|
-
<true/>
|
|
158
|
-
<key>NSRequiresAquaSystemAppearance</key>
|
|
159
|
-
<false/>
|
|
160
|
-
<key>LSUIElement</key>
|
|
161
|
-
<true/>
|
|
162
|
-
<key>NSAppleScriptEnabled</key>
|
|
163
|
-
<false/>
|
|
164
|
-
<key>CFBundleDocumentTypes</key>
|
|
165
|
-
<array/>
|
|
166
|
-
<key>NSPrincipalClass</key>
|
|
167
|
-
<string>NSApplication</string>
|
|
168
|
-
</dict>
|
|
169
|
-
</plist>'''
|
|
170
|
-
|
|
171
|
-
plist_path = self.app_bundle_path / "Contents" / "Info.plist"
|
|
172
|
-
plist_path.write_text(plist_content)
|
|
173
|
-
return True
|
|
174
|
-
|
|
175
|
-
except Exception as e:
|
|
176
|
-
print(f"Error creating Info.plist: {e}")
|
|
177
|
-
return False
|
|
178
|
-
|
|
179
|
-
def create_launch_script(self) -> bool:
|
|
180
|
-
"""Create the executable launch script."""
|
|
181
|
-
try:
|
|
182
|
-
script_content = '''#!/bin/bash
|
|
183
|
-
|
|
184
|
-
# AbstractAssistant macOS App Launcher
|
|
185
|
-
# This script launches the AbstractAssistant application
|
|
186
|
-
|
|
187
|
-
# Set up environment paths for GUI launch (common locations)
|
|
188
|
-
export PATH="/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:$PATH"
|
|
189
|
-
|
|
190
|
-
# Add user-specific Python paths if they exist
|
|
191
|
-
if [ -d "$HOME/.pyenv/shims" ]; then
|
|
192
|
-
export PATH="$HOME/.pyenv/shims:$PATH"
|
|
193
|
-
fi
|
|
194
|
-
|
|
195
|
-
if [ -d "$HOME/.local/bin" ]; then
|
|
196
|
-
export PATH="$HOME/.local/bin:$PATH"
|
|
197
|
-
fi
|
|
198
|
-
|
|
199
|
-
if [ -d "/opt/anaconda3/bin" ]; then
|
|
200
|
-
export PATH="/opt/anaconda3/bin:$PATH"
|
|
201
|
-
fi
|
|
202
|
-
|
|
203
|
-
if [ -d "$HOME/anaconda3/bin" ]; then
|
|
204
|
-
export PATH="$HOME/anaconda3/bin:$PATH"
|
|
205
|
-
fi
|
|
206
|
-
|
|
207
|
-
# Function to find Python with abstractassistant installed
|
|
208
|
-
find_python_with_abstractassistant() {
|
|
209
|
-
# Try specific paths first (more reliable than PATH-based search)
|
|
210
|
-
for python_path in \\
|
|
211
|
-
"$HOME/.pyenv/versions/*/bin/python3" \\
|
|
212
|
-
"$HOME/.pyenv/shims/python3" \\
|
|
213
|
-
"/usr/local/bin/python3" \\
|
|
214
|
-
"/opt/homebrew/bin/python3" \\
|
|
215
|
-
"/usr/bin/python3" \\
|
|
216
|
-
"/opt/anaconda3/bin/python" \\
|
|
217
|
-
"$HOME/anaconda3/bin/python" \\
|
|
218
|
-
"/usr/local/anaconda3/bin/python"; do
|
|
219
|
-
|
|
220
|
-
# Expand glob patterns
|
|
221
|
-
for py in $python_path; do
|
|
222
|
-
if [ -x "$py" ] && "$py" -c "import abstractassistant" 2>/dev/null; then
|
|
223
|
-
echo "$py"
|
|
224
|
-
return 0
|
|
225
|
-
fi
|
|
226
|
-
done
|
|
227
|
-
done
|
|
228
|
-
|
|
229
|
-
# Fallback to PATH-based search
|
|
230
|
-
for python_cmd in python3 python python3.12 python3.11 python3.10 python3.9; do
|
|
231
|
-
if command -v "$python_cmd" >/dev/null 2>&1; then
|
|
232
|
-
if "$python_cmd" -c "import abstractassistant" 2>/dev/null; then
|
|
233
|
-
echo "$python_cmd"
|
|
234
|
-
return 0
|
|
235
|
-
fi
|
|
236
|
-
fi
|
|
237
|
-
done
|
|
238
|
-
|
|
239
|
-
return 1
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
# Find Python with AbstractAssistant
|
|
243
|
-
PYTHON_EXEC=$(find_python_with_abstractassistant)
|
|
244
|
-
|
|
245
|
-
if [ -z "$PYTHON_EXEC" ]; then
|
|
246
|
-
osascript -e 'display dialog "AbstractAssistant not found in any Python installation.\\n\\nPlease install it with:\\npip install abstractassistant\\n\\nOr run the create-app-bundle command after installation." with title "AbstractAssistant" buttons {"OK"} default button "OK" with icon caution'
|
|
247
|
-
exit 1
|
|
248
|
-
fi
|
|
249
|
-
|
|
250
|
-
# Change to a neutral directory to avoid importing development versions
|
|
251
|
-
cd /tmp
|
|
252
|
-
|
|
253
|
-
# Launch the assistant
|
|
254
|
-
exec "$PYTHON_EXEC" -m abstractassistant.cli "$@"'''
|
|
255
|
-
|
|
256
|
-
script_path = self.app_bundle_path / "Contents" / "MacOS" / "AbstractAssistant"
|
|
257
|
-
script_path.write_text(script_content)
|
|
258
|
-
|
|
259
|
-
# Make executable
|
|
260
|
-
os.chmod(script_path, 0o755)
|
|
261
|
-
return True
|
|
262
|
-
|
|
263
|
-
except Exception as e:
|
|
264
|
-
print(f"Error creating launch script: {e}")
|
|
265
|
-
return False
|
|
266
|
-
|
|
267
|
-
def generate_app_bundle(self) -> bool:
|
|
268
|
-
"""Generate the complete macOS app bundle."""
|
|
269
|
-
if not self.is_macos():
|
|
270
|
-
print("macOS app bundle generation is only available on macOS")
|
|
271
|
-
return False
|
|
272
|
-
|
|
273
|
-
if not self.has_permissions():
|
|
274
|
-
print("Insufficient permissions to create app bundle in /Applications")
|
|
275
|
-
print("Please run with sudo or manually copy the app bundle")
|
|
276
|
-
return False
|
|
277
|
-
|
|
278
|
-
print("Creating macOS app bundle...")
|
|
279
|
-
|
|
280
|
-
# Remove existing bundle if it exists
|
|
281
|
-
if self.app_bundle_path.exists():
|
|
282
|
-
shutil.rmtree(self.app_bundle_path)
|
|
283
|
-
|
|
284
|
-
# Create bundle structure
|
|
285
|
-
if not self.create_app_bundle_structure():
|
|
286
|
-
return False
|
|
287
|
-
|
|
288
|
-
# Generate icon
|
|
289
|
-
if not self.generate_app_icon():
|
|
290
|
-
return False
|
|
291
|
-
|
|
292
|
-
# Create Info.plist
|
|
293
|
-
if not self.create_info_plist():
|
|
294
|
-
return False
|
|
295
|
-
|
|
296
|
-
# Create launch script
|
|
297
|
-
if not self.create_launch_script():
|
|
298
|
-
return False
|
|
299
|
-
|
|
300
|
-
print(f"✅ macOS app bundle created successfully!")
|
|
301
|
-
print(f" Location: {self.app_bundle_path}")
|
|
302
|
-
print(f" You can now launch AbstractAssistant from the Dock!")
|
|
303
|
-
|
|
304
|
-
return True
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def create_macos_app_bundle():
|
|
308
|
-
"""Main function to create macOS app bundle during installation."""
|
|
309
|
-
try:
|
|
310
|
-
# Find the package directory
|
|
311
|
-
package_dir = Path(__file__).parent
|
|
312
|
-
|
|
313
|
-
generator = MacOSAppBundleGenerator(package_dir)
|
|
314
|
-
return generator.generate_app_bundle()
|
|
315
|
-
|
|
316
|
-
except Exception as e:
|
|
317
|
-
print(f"Error creating macOS app bundle: {e}")
|
|
318
|
-
return False
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
if __name__ == "__main__":
|
|
322
|
-
success = create_macos_app_bundle()
|
|
323
|
-
sys.exit(0 if success else 1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|