lemonade-sdk 8.1.10__py3-none-any.whl → 8.1.11__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 lemonade-sdk might be problematic. Click here for more details.

Files changed (30) hide show
  1. lemonade/tools/flm/__init__.py +1 -0
  2. lemonade/tools/flm/utils.py +255 -0
  3. lemonade/tools/llamacpp/utils.py +58 -10
  4. lemonade/tools/server/flm.py +137 -0
  5. lemonade/tools/server/llamacpp.py +23 -5
  6. lemonade/tools/server/serve.py +260 -135
  7. lemonade/tools/server/static/js/chat.js +165 -82
  8. lemonade/tools/server/static/js/models.js +87 -54
  9. lemonade/tools/server/static/js/shared.js +5 -3
  10. lemonade/tools/server/static/logs.html +47 -0
  11. lemonade/tools/server/static/styles.css +159 -8
  12. lemonade/tools/server/static/webapp.html +28 -10
  13. lemonade/tools/server/tray.py +94 -38
  14. lemonade/tools/server/utils/macos_tray.py +226 -0
  15. lemonade/tools/server/utils/{system_tray.py → windows_tray.py} +13 -0
  16. lemonade/tools/server/webapp.py +4 -1
  17. lemonade/tools/server/wrapped_server.py +91 -25
  18. lemonade/version.py +1 -1
  19. lemonade_install/install.py +25 -2
  20. {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.11.dist-info}/METADATA +9 -6
  21. {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.11.dist-info}/RECORD +30 -25
  22. lemonade_server/cli.py +103 -14
  23. lemonade_server/model_manager.py +186 -45
  24. lemonade_server/pydantic_models.py +25 -1
  25. lemonade_server/server_models.json +162 -62
  26. {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.11.dist-info}/WHEEL +0 -0
  27. {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.11.dist-info}/entry_points.txt +0 -0
  28. {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.11.dist-info}/licenses/LICENSE +0 -0
  29. {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.11.dist-info}/licenses/NOTICE.md +0 -0
  30. {lemonade_sdk-8.1.10.dist-info → lemonade_sdk-8.1.11.dist-info}/top_level.txt +0 -0
@@ -181,7 +181,7 @@ async function loadModelStandardized(modelId, options = {}) {
181
181
  // Update load button if provided
182
182
  if (loadButton) {
183
183
  loadButton.disabled = true;
184
- loadButton.textContent = '';
184
+ loadButton.textContent = '';
185
185
  }
186
186
 
187
187
  // Update status indicator to show loading state
@@ -246,7 +246,8 @@ async function loadModelStandardized(modelId, options = {}) {
246
246
  // Reset load button if provided
247
247
  if (loadButton) {
248
248
  loadButton.disabled = false;
249
- loadButton.textContent = 'Load';
249
+ loadButton.textContent = '🚀';
250
+ loadButton.classList.remove('loading');
250
251
  }
251
252
 
252
253
  // Reset chat controls
@@ -280,7 +281,8 @@ async function loadModelStandardized(modelId, options = {}) {
280
281
  // Reset load button if provided
281
282
  if (loadButton) {
282
283
  loadButton.disabled = false;
283
- loadButton.textContent = 'Load';
284
+ loadButton.textContent = '🚀';
285
+ loadButton.classList.remove('loading');
284
286
  }
285
287
 
286
288
  // Reset status indicator on error
@@ -0,0 +1,47 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Lemonade Server Logs</title>
6
+ <style>
7
+ body {
8
+ font-family: monospace;
9
+ background: #1e1e1e;
10
+ color: #d4d4d4;
11
+ margin: 0;
12
+ padding: 0;
13
+ }
14
+ #log-container {
15
+ padding: 10px;
16
+ height: 100vh;
17
+ overflow-y: auto;
18
+ white-space: pre-wrap;
19
+ }
20
+ </style>
21
+ </head>
22
+ <body>
23
+ <div id="log-container"></div>
24
+
25
+ <script>
26
+ function stripAnsi(str) {
27
+ return str.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
28
+ }
29
+ const logContainer = document.getElementById("log-container");
30
+ const ws = new WebSocket(`ws://${location.host}/api/v1/logs/ws`);
31
+
32
+ ws.onmessage = (event) => {
33
+ const line = document.createElement("div");
34
+ line.textContent = stripAnsi(event.data);
35
+ logContainer.appendChild(line);
36
+ logContainer.scrollTop = logContainer.scrollHeight; // auto scroll
37
+ };
38
+
39
+ ws.onclose = () => {
40
+ const msg = document.createElement("div");
41
+ msg.textContent = "[Disconnected from log stream]";
42
+ msg.style.color = "red";
43
+ logContainer.appendChild(msg);
44
+ };
45
+ </script>
46
+ </body>
47
+ </html>
@@ -99,6 +99,15 @@ body::before {
99
99
  .brand-title a {
100
100
  color: inherit;
101
101
  text-decoration: none;
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 0.5rem;
105
+ }
106
+
107
+ .brand-icon {
108
+ width: 1.5rem;
109
+ height: 1.5rem;
110
+ vertical-align: middle;
102
111
  }
103
112
 
104
113
  .navbar-links {
@@ -167,6 +176,9 @@ body::before {
167
176
  width: 100%;
168
177
  margin-left: 1rem;
169
178
  margin-right: 1rem;
179
+ /* Removing only the bottom border and shadow for the content tab gap to look nicer */
180
+ box-shadow: 0 -2px 8px rgba(0, 0, 0, 0);
181
+ border-bottom: none;
170
182
  }
171
183
 
172
184
  .tabs {
@@ -213,14 +225,24 @@ body::before {
213
225
  position: relative;
214
226
  }
215
227
 
228
+ /* Wrapper for the select element with embedded status light */
229
+ .model-select-wrapper {
230
+ position: relative;
231
+ display: flex;
232
+ align-items: center;
233
+ }
234
+
216
235
  .status-light {
217
236
  width: 8px;
218
237
  height: 8px;
219
238
  border-radius: 50%;
220
- position: relative;
239
+ position: absolute;
240
+ left: 8px;
241
+ top: 50%;
242
+ transform: translateY(-50%);
221
243
  transition: all var(--transition-fast);
222
244
  flex-shrink: 0;
223
- align-self: center;
245
+ z-index: 10;
224
246
  }
225
247
 
226
248
  .status-light::before {
@@ -288,7 +310,7 @@ body::before {
288
310
 
289
311
  /* Base styles for the select element */
290
312
  .model-select {
291
- padding: 0.5rem 0.75rem;
313
+ padding: 0.5rem 0.75rem 0.5rem 1.5rem;
292
314
  border: 1px solid #ddd;
293
315
  border-radius: 6px;
294
316
  background: #fafafa;
@@ -391,11 +413,18 @@ button:disabled {
391
413
  display: flex;
392
414
  }
393
415
 
416
+ .tab-content-wrapper {
417
+ width: 85%;
418
+ }
419
+
394
420
  .tab-content {
395
421
  display: none;
396
422
  padding: 2em;
397
423
  background: #fafafa;
398
424
  border-radius: 0 0 8px 8px;
425
+ /* adding border and shadow that was removed for the gap look from higher div */
426
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
427
+ border: 1px solid #e0e0e0;
399
428
  }
400
429
 
401
430
  .tab-content.active {
@@ -406,7 +435,9 @@ button:disabled {
406
435
  .chat-container {
407
436
  display: flex;
408
437
  flex-direction: column;
409
- min-height: 300px;
438
+ /* Max space available in viewport
439
+ This also prevents the chat-history section to resize lower than a certain point */
440
+ min-height: calc(100vh - 550px);
410
441
  min-width: 300px;
411
442
  max-width: 100%;
412
443
  width: 100%;
@@ -414,18 +445,37 @@ button:disabled {
414
445
  border: 1px solid #e0e0e0;
415
446
  border-radius: 8px;
416
447
  background: #fff;
417
- resize: both;
418
- overflow: auto;
448
+ /* Use a semi-fixed chat 'window' height so streaming content never expands the page.
449
+ The chat-history area inside will scroll. The CSS variable allows easy tuning. */
450
+ --chat-height: 520px;
451
+ /* Allow vertical resizing by the user while keeping horizontal size fixed.
452
+ Constrain the resize with min/max heights so it stays usable and doesn't overflow the viewport. */
453
+ resize: vertical;
454
+ height: var(--chat-height);
455
+ max-height: calc(100vh - 120px);
456
+ overflow: hidden; /* hide overflow at container level; chat-history will scroll */
457
+ }
458
+
459
+ /* Responsive fallback: if the viewport height is small, cap the chat window to fit */
460
+ @media (max-height: 700px) {
461
+ .chat-container {
462
+ height: calc(100vh - 180px); /* leave space for navbar/footer */
463
+ }
419
464
  }
420
465
 
421
466
  .chat-history {
422
- flex: 1;
467
+ /* Make chat history take remaining space in the chat container and scroll when content
468
+ exceeds this space. This prevents streaming text from expanding the overall layout. */
469
+ flex: 1 1 auto;
423
470
  overflow-y: auto;
471
+ -webkit-overflow-scrolling: touch;
424
472
  padding: 1em;
425
473
  border-bottom: 1px solid #e0e0e0;
426
474
  display: flex;
427
475
  flex-direction: column;
428
476
  gap: 0.5em;
477
+ /* Optional visual hint for scrollable content */
478
+ scrollbar-width: thin;
429
479
  }
430
480
 
431
481
  .chat-message {
@@ -1492,6 +1542,11 @@ button:disabled {
1492
1542
  font-size: 1.3rem;
1493
1543
  }
1494
1544
 
1545
+ .brand-icon {
1546
+ width: 1.3rem;
1547
+ height: 1.3rem;
1548
+ }
1549
+
1495
1550
  .navbar-links {
1496
1551
  gap: 1.5rem;
1497
1552
  font-size: 1rem;
@@ -1507,6 +1562,11 @@ button:disabled {
1507
1562
  font-size: 1.2rem;
1508
1563
  }
1509
1564
 
1565
+ .brand-icon {
1566
+ width: 1.2rem;
1567
+ height: 1.2rem;
1568
+ }
1569
+
1510
1570
  .navbar-links {
1511
1571
  gap: 1rem;
1512
1572
  font-size: 0.9rem;
@@ -1838,6 +1898,53 @@ button:disabled {
1838
1898
  align-items: center;
1839
1899
  }
1840
1900
 
1901
+ /* FastFlowLM notice styles */
1902
+ .flm-notice {
1903
+ background: linear-gradient(135deg, #fff3cd 0%, #ffeaa7 100%);
1904
+ border: 1px solid #ffeaa7;
1905
+ border-left: 4px solid #f39c12;
1906
+ border-radius: 8px;
1907
+ margin-bottom: 1.5rem;
1908
+ box-shadow: 0 2px 8px rgba(243, 156, 18, 0.1);
1909
+ animation: fadeIn 0.3s ease;
1910
+ }
1911
+
1912
+ .flm-notice-content {
1913
+ display: flex;
1914
+ align-items: flex-start;
1915
+ gap: 0.75rem;
1916
+ padding: 1rem 1.25rem;
1917
+ }
1918
+
1919
+ .flm-notice-icon {
1920
+ font-size: 1.2rem;
1921
+ flex-shrink: 0;
1922
+ margin-top: 0.1rem;
1923
+ }
1924
+
1925
+ .flm-notice-text {
1926
+ flex: 1;
1927
+ font-size: 0.95rem;
1928
+ line-height: 1.5;
1929
+ color: #856404;
1930
+ }
1931
+
1932
+ .flm-notice-text strong {
1933
+ color: #6c5ce7;
1934
+ font-weight: 600;
1935
+ }
1936
+
1937
+ .flm-notice-text a {
1938
+ color: var(--info-primary);
1939
+ text-decoration: none;
1940
+ font-weight: 500;
1941
+ }
1942
+
1943
+ .flm-notice-text a:hover {
1944
+ text-decoration: underline;
1945
+ color: var(--info-hover);
1946
+ }
1947
+
1841
1948
  .error-banner .close-btn {
1842
1949
  background: none;
1843
1950
  border: none;
@@ -2275,4 +2382,48 @@ button:disabled {
2275
2382
  0 8px 25px rgba(200, 88, 108, 0.2),
2276
2383
  0 3px 10px rgba(0, 0, 0, 0.1),
2277
2384
  inset 0 1px 0 rgba(255, 255, 255, 0.9);
2278
- }
2385
+ }
2386
+
2387
+
2388
+ /* Dropdown styling */
2389
+ .dropdown {
2390
+ position: relative;
2391
+ display: inline-block;
2392
+ }
2393
+
2394
+ .dropbtn {
2395
+ background-color: transparent;
2396
+ border: none;
2397
+ font-size: 16px;
2398
+ cursor: pointer;
2399
+ }
2400
+
2401
+ .dropdown-content {
2402
+ display: none;
2403
+ position: absolute;
2404
+ right: 0; /* align to the right edge of the parent */
2405
+ left: auto; /* prevent left alignment */
2406
+ top: calc(100% + 1px); /* opens 8px below the button */
2407
+ background-color: #ffe76c;
2408
+ border-radius: 8px;
2409
+ min-width: 140px;
2410
+ box-shadow: 0px 8px 16px rgba(0,0,0,0.2);
2411
+ z-index: 1000;
2412
+ overflow: hidden;
2413
+ }
2414
+
2415
+ .dropdown-content a {
2416
+ color: black;
2417
+ font-size: 12px; /* smaller font */
2418
+ padding: 8px 12px;
2419
+ text-decoration: none;
2420
+ display: block;
2421
+ }
2422
+
2423
+ .dropdown-content a:hover {
2424
+ background-color: #f6c146;
2425
+ }
2426
+
2427
+ .dropdown:hover .dropdown-content {
2428
+ display: block;
2429
+ }
@@ -10,12 +10,13 @@
10
10
  window.SERVER_PORT = {{SERVER_PORT}};
11
11
  </script>
12
12
  {{SERVER_MODELS_JS}}
13
+ {{PLATFORM_JS}}
13
14
  </head>
14
15
  <body>
15
16
  <nav class="navbar" id="navbar">
16
17
  <div class="navbar-brand">
17
- <span class="brand-title"><a href="https://lemonade-server.ai">🍋 Lemonade Server</a></span>
18
- </div>
18
+ <span class="brand-title"><a href="https://lemonade-server.ai"><img src="/static/favicon.ico" alt="🍋" class="brand-icon"> Lemonade Server</a></span>
19
+ </div>
19
20
  <div class="navbar-links">
20
21
  <a href="https://github.com/lemonade-sdk/lemonade" target="_blank">GitHub</a>
21
22
  <a href="https://lemonade-server.ai/docs/" target="_blank">Docs</a>
@@ -29,6 +30,7 @@
29
30
  <button class="close-btn" onclick="hideErrorBanner()">&times;</button>
30
31
  </div>
31
32
  <main class="main">
33
+ <div class="tab-content-wrapper">
32
34
  <div class="tab-container">
33
35
  <div class="tabs">
34
36
  <div class="tab-group">
@@ -38,23 +40,32 @@
38
40
  </div>
39
41
 
40
42
  <div class="model-status-indicator" id="model-status-indicator">
41
- <div class="status-light" id="status-light"></div>
42
- <select id="model-select" class="model-select">
43
- <option value="">Pick a model</option>
44
- </select>
43
+ <div class="model-select-wrapper">
44
+ <div class="status-light" id="status-light"></div>
45
+ <select id="model-select" class="model-select">
46
+ <option value="">Pick a model</option>
47
+ </select>
48
+ </div>
45
49
  <button class="model-action-btn" id="model-unload-btn" title="Unload model" style="display: flex;">⏏</button>
50
+ <!-- Dropdown -->
51
+ <div class="dropdown">
52
+ <button class="dropbtn" aria-label="Dropdown" style="display: flex;">🛠️</button>
53
+ <div class="dropdown-content">
54
+ <a href="/static/logs.html" target="_blank">View Logs</a>
55
+ </div>
56
+ </div>
46
57
  </div>
47
- </div>
58
+ </div>
48
59
  <div class="tab-content active" id="content-chat">
49
60
  <div class="chat-container">
50
- <div class="chat-history" id="chat-history"></div>
61
+ <div class="chat-history" id="chat-history" style="overflow-y: auto;"></div>
51
62
  <div class="chat-input-row">
52
63
  <div class="input-with-indicator">
53
64
  <textarea id="chat-input" placeholder="Type your message..." rows="1"></textarea>
54
65
  </div>
55
66
  <input type="file" id="file-attachment" style="display: none;" multiple accept="image/*">
56
67
  <button id="attachment-btn" title="Attach files">&#x1F4CE;</button>
57
- <button id="send-btn">Send</button>
68
+ <button id="toggle-btn" title="Start">Start</button>
58
69
  </div>
59
70
  <div class="attachments-preview-container" id="attachments-preview-container">
60
71
  <div class="attachments-preview-row" id="attachments-preview-row"></div>
@@ -150,6 +161,7 @@
150
161
  <div class="subcategory" data-recipe="oga-hybrid" onclick="selectRecipe('oga-hybrid')">OGA Hybrid</div>
151
162
  <div class="subcategory" data-recipe="oga-npu" onclick="selectRecipe('oga-npu')">OGA NPU</div>
152
163
  <div class="subcategory" data-recipe="oga-cpu" onclick="selectRecipe('oga-cpu')">OGA CPU</div>
164
+ <div class="subcategory" data-recipe="flm" onclick="selectRecipe('flm')">FastFlowLM NPU</div>
153
165
  </div>
154
166
  </div>
155
167
  <div class="model-category-section">
@@ -204,6 +216,7 @@
204
216
  </label>
205
217
  <select id="register-recipe" name="recipe" required>
206
218
  <option value="llamacpp">llamacpp</option>
219
+ <option value="flm">flm</option>
207
220
  <option value="oga-npu">oga-npu</option>
208
221
  <option value="oga-hybrid">oga-hybrid</option>
209
222
  <option value="oga-cpu">oga-cpu</option>
@@ -221,6 +234,11 @@
221
234
  Reasoning
222
235
  <span class="tooltip-icon" data-tooltip="Enable to inform Lemonade Server that the model has reasoning capabilities that will use thinking tokens.">ⓘ</span>
223
236
  </label>
237
+ <label class="register-label reasoning-inline">
238
+ <input type="checkbox" id="register-vision" name="vision">
239
+ Vision
240
+ <span class="tooltip-icon" data-tooltip="Enable to inform Lemonade Server that the model has vision capabilities for processing images.">ⓘ</span>
241
+ </label>
224
242
  </div>
225
243
  <div class="register-form-row register-form-row-tight">
226
244
  <button type="submit" id="register-submit">Install</button>
@@ -231,6 +249,7 @@
231
249
  </div>
232
250
  </div>
233
251
  </div>
252
+ </div>
234
253
  </div>
235
254
  </main>
236
255
  <footer class="site-footer">
@@ -250,4 +269,3 @@
250
269
  <script src="/static/js/chat.js"></script>
251
270
  </body>
252
271
  </html>
253
- </html>
@@ -7,6 +7,7 @@ import webbrowser
7
7
  from pathlib import Path
8
8
  import logging
9
9
  import tempfile
10
+ import platform
10
11
 
11
12
  import requests
12
13
  from packaging.version import parse as parse_version
@@ -14,7 +15,20 @@ from packaging.version import parse as parse_version
14
15
  from lemonade_server.pydantic_models import DEFAULT_CTX_SIZE
15
16
 
16
17
  from lemonade.version import __version__
17
- from lemonade.tools.server.utils.system_tray import SystemTray, Menu, MenuItem
18
+
19
+ # Import the appropriate tray implementation based on platform
20
+ if platform.system() == "Darwin": # macOS
21
+ from lemonade.tools.server.utils.macos_tray import (
22
+ MacOSSystemTray as SystemTray,
23
+ Menu,
24
+ MenuItem,
25
+ )
26
+ else: # Windows/Linux
27
+ from lemonade.tools.server.utils.windows_tray import (
28
+ SystemTray,
29
+ Menu,
30
+ MenuItem,
31
+ )
18
32
 
19
33
 
20
34
  class OutputDuplicator:
@@ -87,6 +101,9 @@ class LemonadeTray(SystemTray):
87
101
  self.version_check_thread = None
88
102
  self.stop_version_check = threading.Event()
89
103
 
104
+ # Hook function for platform-specific initialization callback
105
+ self.on_ready = None
106
+
90
107
  def get_latest_version(self):
91
108
  """
92
109
  Update the latest version information.
@@ -191,15 +208,38 @@ class LemonadeTray(SystemTray):
191
208
  Show the log file in a new window.
192
209
  """
193
210
  try:
194
- subprocess.Popen(
195
- [
196
- "powershell",
197
- "Start-Process",
198
- "powershell",
199
- "-ArgumentList",
200
- f'"-NoExit", "Get-Content -Wait {self.log_file}"',
201
- ]
202
- )
211
+ system = platform.system().lower()
212
+ if system == "darwin":
213
+ # Use Terminal.app to show live logs on macOS
214
+ try:
215
+ subprocess.Popen(
216
+ [
217
+ "osascript",
218
+ "-e",
219
+ f'tell application "Terminal" to do script "tail -f {self.log_file}"',
220
+ ]
221
+ )
222
+ except (subprocess.CalledProcessError, FileNotFoundError) as e:
223
+ self.logger.error(f"Failed to open Terminal for logs: {e}")
224
+ self.show_balloon_notification(
225
+ "Error",
226
+ f"Failed to open logs in Terminal. Log file: {self.log_file}",
227
+ )
228
+ elif system == "windows":
229
+ # Use PowerShell on Windows
230
+ subprocess.Popen(
231
+ [
232
+ "powershell",
233
+ "Start-Process",
234
+ "powershell",
235
+ "-ArgumentList",
236
+ f'"-NoExit", "Get-Content -Wait {self.log_file}"',
237
+ ]
238
+ )
239
+ else:
240
+ # Unsupported platform
241
+ self.logger.error(f"Log viewing not supported on platform: {system}")
242
+
203
243
  except Exception as e: # pylint: disable=broad-exception-caught
204
244
  self.logger.error(f"Error opening logs: {str(e)}")
205
245
 
@@ -228,7 +268,7 @@ class LemonadeTray(SystemTray):
228
268
  try:
229
269
  response = requests.get(
230
270
  f"http://localhost:{self.port}/api/v0/health",
231
- timeout=0.1, # Add timeout
271
+ timeout=0.1,
232
272
  )
233
273
  response.raise_for_status()
234
274
  response_data = response.json()
@@ -257,7 +297,9 @@ class LemonadeTray(SystemTray):
257
297
  """
258
298
  Change the server port and restart the server.
259
299
  """
300
+
260
301
  try:
302
+
261
303
  # Stop the current server
262
304
  if self.server_thread and self.server_thread.is_alive():
263
305
  # Set should_exit flag on the uvicorn server instance
@@ -270,16 +312,17 @@ class LemonadeTray(SystemTray):
270
312
 
271
313
  # Update the port in both the tray and the server instance
272
314
  self.port = new_port
273
- if self.server:
274
- self.server.port = new_port
275
315
 
276
- # Restart the server
316
+ # Clear the old server instance to ensure a fresh start
317
+ # This prevents middleware conflicts when restarting
318
+ self.server = None
319
+
277
320
  self.server_thread = threading.Thread(target=self.start_server, daemon=True)
278
321
  self.server_thread.start()
279
322
 
280
- # Show notification
281
323
  self.show_balloon_notification(
282
- "Port Changed", f"Lemonade Server is now running on port {self.port}"
324
+ "Port Changed",
325
+ f"Lemonade Server is now running on port {self.port}",
283
326
  )
284
327
 
285
328
  except Exception as e: # pylint: disable=broad-exception-caught
@@ -539,6 +582,11 @@ class LemonadeTray(SystemTray):
539
582
  Start the uvicorn server.
540
583
  """
541
584
  self.server = self.server_factory()
585
+
586
+ # Ensure the server uses the current port from the tray
587
+ # This is important when changing ports
588
+ self.server.port = self.port
589
+
542
590
  self.server.uvicorn_server = self.server.run_in_thread(self.server.host)
543
591
  self.server.uvicorn_server.run()
544
592
 
@@ -547,16 +595,6 @@ class LemonadeTray(SystemTray):
547
595
  Run the Lemonade tray application.
548
596
  """
549
597
 
550
- # Register window class and create window
551
- self.register_window_class()
552
- self.create_window()
553
-
554
- # Set up Windows console control handler for CTRL+C
555
- self.console_handler = self.setup_console_control_handler(self.logger)
556
-
557
- # Add tray icon
558
- self.add_tray_icon()
559
-
560
598
  # Start the background model mapping update thread
561
599
  self.model_update_thread = threading.Thread(
562
600
  target=self.update_downloaded_models_background, daemon=True
@@ -573,17 +611,27 @@ class LemonadeTray(SystemTray):
573
611
  self.server_thread = threading.Thread(target=self.start_server, daemon=True)
574
612
  self.server_thread.start()
575
613
 
576
- # Show initial notification
577
- self.show_balloon_notification(
578
- "Woohoo!",
579
- (
580
- "Lemonade Server is running! "
581
- "Right-click the tray icon below to access options."
582
- ),
583
- )
614
+ # Provide an on_ready hook that Windows base tray will call after
615
+ # the HWND/icon are created. macOS will call it immediately after run.
616
+ def _on_ready():
617
+ system = platform.system().lower()
618
+ if system == "darwin":
619
+ message = (
620
+ "Lemonade Server is running! "
621
+ "Click the tray icon above to access options."
622
+ )
623
+ else: # Windows/Linux
624
+ message = (
625
+ "Lemonade Server is running! "
626
+ "Right-click the tray icon below to access options."
627
+ )
628
+ self.show_balloon_notification("Woohoo!", message)
584
629
 
585
- # Run the message loop in the main thread
586
- self.message_loop()
630
+ # Attach hook for both implementations to invoke after init
631
+ self.on_ready = _on_ready
632
+
633
+ # Call the parent run method which handles platform-specific initialization
634
+ super().run()
587
635
 
588
636
  def exit_app(self, icon, item):
589
637
  """
@@ -598,8 +646,16 @@ class LemonadeTray(SystemTray):
598
646
  if self.version_check_thread and self.version_check_thread.is_alive():
599
647
  self.version_check_thread.join(timeout=1)
600
648
 
601
- # Call parent exit method
602
- super().exit_app(icon, item)
649
+ # Platform-specific exit handling
650
+ system = platform.system().lower()
651
+ if system == "darwin": # macOS
652
+ # For macOS, quit the rumps application
653
+ import rumps
654
+
655
+ rumps.quit_application()
656
+ else:
657
+ # Call parent exit method for Windows
658
+ super().exit_app(icon, item)
603
659
 
604
660
  # Stop the server using the CLI stop command to ensure a rigorous cleanup
605
661
  # This must be a subprocess to ensure the cleanup doesnt kill itself