reasoning-deployment-service 0.2.8__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 reasoning-deployment-service might be problematic. Click here for more details.

Files changed (29) hide show
  1. examples/programmatic_usage.py +154 -0
  2. reasoning_deployment_service/__init__.py +25 -0
  3. reasoning_deployment_service/cli_editor/__init__.py +5 -0
  4. reasoning_deployment_service/cli_editor/api_client.py +666 -0
  5. reasoning_deployment_service/cli_editor/cli_runner.py +343 -0
  6. reasoning_deployment_service/cli_editor/config.py +82 -0
  7. reasoning_deployment_service/cli_editor/google_deps.py +29 -0
  8. reasoning_deployment_service/cli_editor/reasoning_engine_creator.py +448 -0
  9. reasoning_deployment_service/gui_editor/__init__.py +5 -0
  10. reasoning_deployment_service/gui_editor/main.py +280 -0
  11. reasoning_deployment_service/gui_editor/requirements_minimal.txt +54 -0
  12. reasoning_deployment_service/gui_editor/run_program.sh +55 -0
  13. reasoning_deployment_service/gui_editor/src/__init__.py +1 -0
  14. reasoning_deployment_service/gui_editor/src/core/__init__.py +1 -0
  15. reasoning_deployment_service/gui_editor/src/core/api_client.py +647 -0
  16. reasoning_deployment_service/gui_editor/src/core/config.py +43 -0
  17. reasoning_deployment_service/gui_editor/src/core/google_deps.py +22 -0
  18. reasoning_deployment_service/gui_editor/src/core/reasoning_engine_creator.py +448 -0
  19. reasoning_deployment_service/gui_editor/src/ui/__init__.py +1 -0
  20. reasoning_deployment_service/gui_editor/src/ui/agent_space_view.py +312 -0
  21. reasoning_deployment_service/gui_editor/src/ui/authorization_view.py +280 -0
  22. reasoning_deployment_service/gui_editor/src/ui/reasoning_engine_view.py +354 -0
  23. reasoning_deployment_service/gui_editor/src/ui/reasoning_engines_view.py +204 -0
  24. reasoning_deployment_service/gui_editor/src/ui/ui_components.py +1221 -0
  25. reasoning_deployment_service/reasoning_deployment_service.py +687 -0
  26. reasoning_deployment_service-0.2.8.dist-info/METADATA +177 -0
  27. reasoning_deployment_service-0.2.8.dist-info/RECORD +29 -0
  28. reasoning_deployment_service-0.2.8.dist-info/WHEEL +5 -0
  29. reasoning_deployment_service-0.2.8.dist-info/top_level.txt +2 -0
@@ -0,0 +1,354 @@
1
+ """Reasoning Engine management view."""
2
+ import tkinter as tk
3
+ from tkinter import ttk, messagebox
4
+ from typing import Callable
5
+
6
+ from ..core.api_client import ApiClient
7
+ from .ui_components import async_operation, StatusButton, DeployDialog, EngineDetailsDialog, LoadingDialog, CreateReasoningEngineDialog, CreateReasoningEngineAdvancedDialog
8
+
9
+
10
+ class ReasoningEngineView(ttk.Frame):
11
+ """UI for engine lifecycle."""
12
+
13
+ def __init__(self, master, api: ApiClient, log: Callable[[str], None], refresh_agents: Callable[[], None]):
14
+ super().__init__(master)
15
+ self.api = api
16
+ self.log = log
17
+ self.refresh_agents = refresh_agents
18
+ self._engines_auto_loaded = False # Track if engines have been auto-loaded
19
+
20
+ # Cache authentication state to avoid repeated API calls
21
+ self._cached_auth_state = None
22
+ self._last_auth_check = 0
23
+ self._auth_cache_duration = 30 # 30 seconds
24
+
25
+ self._setup_ui()
26
+
27
+ def _setup_ui(self):
28
+ # Action buttons with prominent CREATE ENGINE NOW button
29
+ actions = ttk.Frame(self)
30
+ actions.pack(fill="x", padx=4, pady=6)
31
+
32
+ # Left side - secondary buttons
33
+ left_actions = ttk.Frame(actions)
34
+ left_actions.pack(side="left")
35
+
36
+ # Right side - prominent CREATE ENGINE NOW button
37
+ right_actions = ttk.Frame(actions)
38
+ right_actions.pack(side="right")
39
+
40
+ # All Engines section
41
+ engines_frame = ttk.LabelFrame(self, text="All Reasoning Engines", padding=10)
42
+ engines_frame.pack(fill="both", expand=True, padx=4, pady=(16, 6))
43
+
44
+ # Engines control buttons
45
+ engines_btns = ttk.Frame(engines_frame)
46
+ engines_btns.pack(fill="x", pady=(0, 4))
47
+
48
+ self.refresh_engines_btn = StatusButton(engines_btns, text="Refresh All Engines", command=self._refresh_engines)
49
+ self.refresh_engines_btn.pack(side="left", padx=4)
50
+
51
+ self.delete_engines_btn = StatusButton(engines_btns, text="Delete Selected Engines", command=self._delete_selected_engines)
52
+ self.delete_engines_btn.pack(side="left", padx=8)
53
+
54
+ self.engine_details_btn = StatusButton(engines_btns, text="More Engine Details", command=self.show_engine_details)
55
+ self.engine_details_btn.pack(side="left", padx=8)
56
+
57
+ self.engines_status = tk.StringVar(value="Ready.")
58
+ ttk.Label(engines_btns, textvariable=self.engines_status).pack(side="right")
59
+
60
+ # Engines list
61
+ engines_wrap = ttk.Frame(engines_frame)
62
+ engines_wrap.pack(fill="both", expand=True)
63
+ engines_cols = ("id", "display_name", "create_time")
64
+ self.engines_tree = ttk.Treeview(engines_wrap, columns=engines_cols, show="headings", selectmode="extended")
65
+
66
+ for c, t, w in [
67
+ ("id", "Engine ID", 250),
68
+ ("display_name", "Display Name", 400),
69
+ ("create_time", "Created", 200),
70
+ ]:
71
+ self.engines_tree.heading(c, text=t)
72
+ self.engines_tree.column(c, width=w, anchor="w")
73
+
74
+ self.engines_tree.pack(side="left", fill="both", expand=True)
75
+ engines_vsb = ttk.Scrollbar(engines_wrap, orient="vertical", command=self.engines_tree.yview)
76
+ self.engines_tree.configure(yscroll=engines_vsb.set)
77
+ engines_vsb.pack(side="right", fill="y")
78
+
79
+ # Engines event bindings
80
+ self.engines_tree.bind("<<TreeviewSelect>>", self._on_engines_selection_change)
81
+ self.engines_tree.bind("<Button-3>", self._engines_popup)
82
+
83
+ # Engines context menu
84
+ self.engines_menu = tk.Menu(self, tearoff=0)
85
+ self.engines_menu.add_command(label="Delete", command=self._delete_selected_engines)
86
+
87
+ # Debouncing timers
88
+ self._update_timer = None
89
+ self._engines_update_timer = None
90
+
91
+ # Store full engine data for details popup
92
+ self._engines_data = {}
93
+
94
+ # Status
95
+ self.eng_status = tk.StringVar(value="Ready")
96
+ ttk.Label(self, textvariable=self.eng_status).pack(fill="x", padx=4, pady=(8, 0))
97
+
98
+ # Initialize button states without immediate API calls
99
+ self._update_button_states()
100
+ # Don't auto-refresh on startup - let user click refresh button
101
+
102
+ def _get_cached_auth_state(self) -> bool:
103
+ """Get authentication state with local caching to reduce API calls."""
104
+ import time
105
+ now = time.time()
106
+
107
+ # Use cached result if still fresh
108
+ if (self._cached_auth_state is not None and
109
+ (now - self._last_auth_check) < self._auth_cache_duration):
110
+ return self._cached_auth_state
111
+
112
+ # Check authentication and cache result
113
+ self._cached_auth_state = self.api.is_authenticated
114
+ self._last_auth_check = now
115
+ return self._cached_auth_state
116
+
117
+ def _status_text(self) -> str:
118
+ return f"Engine: {self.engine_name_var.get()} | Agent: {self.agent_space_id_var.get()}"
119
+
120
+ def _update_button_states(self):
121
+ """Update button states based on current conditions."""
122
+ # Use cached authentication state to reduce API calls
123
+ has_auth = self._get_cached_auth_state()
124
+ has_engine = self.api.has_engine
125
+ has_agent = self.api.has_deployed_agent
126
+
127
+ # Create advanced engine - enabled if authenticated
128
+ # self.create_advanced_btn.set_enabled(
129
+ # has_auth,
130
+ # "Authentication required" if not has_auth else ""
131
+ # )
132
+
133
+ # Engines list buttons
134
+ self.refresh_engines_btn.set_enabled(
135
+ has_auth,
136
+ "Authentication required" if not has_auth else ""
137
+ )
138
+
139
+ engines_selection = self.engines_tree.selection()
140
+ engines_selection_count = len(engines_selection)
141
+
142
+ self.delete_engines_btn.set_enabled(
143
+ engines_selection_count > 0,
144
+ "Select engines to delete" if engines_selection_count == 0 else ""
145
+ )
146
+
147
+ # Deploy - enabled if authenticated and exactly one engine is selected
148
+ single_engine_selected = engines_selection_count == 1
149
+ # self.deploy_btn.set_enabled(
150
+ # has_auth and single_engine_selected,
151
+ # "Authentication required" if not has_auth else
152
+ # "Select exactly one engine to deploy" if not single_engine_selected else ""
153
+ # )
154
+
155
+ # Engine details button - enabled only if a single engine is selected
156
+ self.engine_details_btn.set_enabled(
157
+ single_engine_selected,
158
+ "Select a single engine to view details" if not single_engine_selected else ""
159
+ )
160
+
161
+ def _on_engines_selection_change(self, event=None):
162
+ """Handle engines tree selection changes - IMMEDIATE update, no debouncing."""
163
+ # Immediate update - no timers or delays
164
+ self._update_button_states()
165
+
166
+ def _refresh_engines(self):
167
+ """Refresh the list of all reasoning engines."""
168
+ # Update button states immediately on click
169
+ self._update_button_states()
170
+
171
+ # Check authentication once at the start using cached state
172
+ if not self._get_cached_auth_state():
173
+ self.log("❌ Authentication required")
174
+ return
175
+
176
+ # Show loading dialog
177
+ loading_dialog = LoadingDialog(self.winfo_toplevel(), "Loading reasoning engines...")
178
+ self.refresh_engines_btn.set_enabled(False, "Loading...")
179
+
180
+ def callback(items):
181
+ # Close loading dialog
182
+ loading_dialog.close()
183
+ self.refresh_engines_btn.set_enabled(True)
184
+
185
+ for i in self.engines_tree.get_children():
186
+ self.engines_tree.delete(i)
187
+
188
+ # Clear stored data
189
+ self._engines_data.clear()
190
+
191
+ if isinstance(items, Exception):
192
+ self.log(f"❌ Engines list error: {items}")
193
+ self.engines_status.set("Error")
194
+ return
195
+
196
+ for it in items:
197
+ # Format create time nicely
198
+ create_time = it.get("create_time", "Unknown")
199
+ if create_time != "Unknown" and "T" in str(create_time):
200
+ create_time = str(create_time).split("T")[0] # Just the date part
201
+
202
+ # Insert into tree with only 3 columns
203
+ item_id = self.engines_tree.insert("", "end", values=(
204
+ it["id"],
205
+ it["display_name"],
206
+ create_time
207
+ ))
208
+
209
+ # Store full data for popup using tree item ID as key
210
+ self._engines_data[item_id] = it
211
+
212
+ self.engines_status.set(f"{len(items)} engine(s)")
213
+ self._update_button_states()
214
+
215
+ async_operation(self.api.list_reasoning_engines, callback=callback, ui_widget=self)
216
+
217
+ def _delete_selected_engines(self):
218
+ """Delete selected reasoning engines."""
219
+ sel = self.engines_tree.selection()
220
+ if not sel:
221
+ messagebox.showinfo("No selection", "Select one or more engines to delete.")
222
+ return
223
+
224
+ # Get resource names from stored data
225
+ resource_names = []
226
+ for item_id in sel:
227
+ if item_id in self._engines_data:
228
+ resource_names.append(self._engines_data[item_id]["resource_name"])
229
+
230
+ if not resource_names:
231
+ messagebox.showerror("Error", "Could not find resource names for selected engines.")
232
+ return
233
+
234
+ if not messagebox.askyesno("Confirm delete",
235
+ f"Delete {len(resource_names)} selected engine(s)?\n\n"
236
+ "⚠️ This will permanently delete the reasoning engines!"):
237
+ return
238
+
239
+ self.engines_status.set("Deleting…")
240
+ self.delete_engines_btn.set_enabled(False, "Deleting...")
241
+
242
+ def ui_log(msg: str):
243
+ """Thread-safe logging - marshal to UI thread."""
244
+ self.after(0, lambda: self.log(msg))
245
+
246
+ def batch_delete():
247
+ success_count = 0
248
+ for resource_name in resource_names:
249
+ status, msg = self.api.delete_reasoning_engine_by_id(resource_name)
250
+ ui_log(f"{status.upper()}: {msg} — {resource_name}")
251
+ if status == "deleted":
252
+ success_count += 1
253
+ return success_count == len(resource_names)
254
+
255
+ def callback(success):
256
+ self.delete_engines_btn.set_enabled(True)
257
+ self._refresh_engines()
258
+ status_msg = "✅ Delete operation completed." if success else "⚠️ Delete completed with issues."
259
+ self.log(status_msg)
260
+
261
+ async_operation(batch_delete, callback=callback, ui_widget=self)
262
+
263
+ def show_engine_details(self):
264
+ """Show details for the selected engine."""
265
+ sel = self.engines_tree.selection()
266
+ if len(sel) != 1:
267
+ messagebox.showinfo("No selection", "Select a single engine to view details.")
268
+ return
269
+
270
+ item_id = sel[0]
271
+ if item_id not in self._engines_data:
272
+ messagebox.showerror("Error", "Engine data not found.")
273
+ return
274
+
275
+ engine_data = self._engines_data[item_id]
276
+ try:
277
+ EngineDetailsDialog(self, engine_data)
278
+ except Exception as e:
279
+ print(f"Error opening engine details dialog: {e}")
280
+ messagebox.showerror("Error", f"Failed to open details dialog: {e}")
281
+
282
+ def _engines_popup(self, event):
283
+ """Show engines context menu."""
284
+ row = self.engines_tree.identify_row(event.y)
285
+ if row:
286
+ if row not in self.engines_tree.selection():
287
+ self.engines_tree.selection_set(row)
288
+ try:
289
+ self.engines_menu.tk_popup(event.x_root, event.y_root)
290
+ finally:
291
+ self.engines_menu.grab_release()
292
+
293
+ def _create_engine_advanced(self):
294
+ """Create a new reasoning engine with advanced configuration."""
295
+ if not self.api.is_authenticated:
296
+ self.log("❌ Authentication required")
297
+ return
298
+
299
+ # Show advanced create engine dialog
300
+ dialog = CreateReasoningEngineAdvancedDialog(self.winfo_toplevel(), self.api)
301
+ self.wait_window(dialog)
302
+
303
+ if not dialog.result:
304
+ return # User cancelled
305
+
306
+ config = dialog.result
307
+ self.log(f"⚙️ Creating advanced reasoning engine '{config['display_name']}'...")
308
+ # self.create_advanced_btn.set_enabled(False, "Creating...")
309
+
310
+ def callback(res):
311
+ # self.create_advanced_btn.set_enabled(True)
312
+ if isinstance(res, Exception):
313
+ self.log(f"❌ {res}")
314
+ return
315
+
316
+ status, msg, resource = res
317
+ self.log(f"{status.upper()}: {msg}")
318
+ if resource:
319
+ self.log(f"resource: {resource}")
320
+
321
+ # Refresh engines list to show the new engine
322
+ self._refresh_engines()
323
+ self._update_button_states()
324
+
325
+ async_operation(lambda: self.api.create_reasoning_engine_advanced(config), callback=callback, ui_widget=self)
326
+
327
+ def update_api(self, api: ApiClient):
328
+ """Update the API client reference."""
329
+ self.api = api
330
+ # Clear cached auth state when API changes
331
+ self._cached_auth_state = None
332
+ self._last_auth_check = 0
333
+ # Reset auto-load flag
334
+ self._engines_auto_loaded = False
335
+ # Update button states immediately
336
+ self._update_button_states()
337
+
338
+ # Auto-load engines if credentials are available and not loaded yet
339
+ if self._get_cached_auth_state():
340
+ self._engines_auto_loaded = True
341
+ self.log("✅ Auto-loading reasoning engines...")
342
+ # Use a small delay to ensure UI is ready
343
+ self.after(50, self._refresh_engines)
344
+
345
+ def on_tab_selected(self):
346
+ """Called when this tab is selected - trigger auto-loading if needed."""
347
+ try:
348
+ if not self._engines_auto_loaded and self._get_cached_auth_state():
349
+ self._engines_auto_loaded = True
350
+ self.log("✅ Auto-loading reasoning engines...")
351
+ self._refresh_engines()
352
+ except Exception as e:
353
+ self.log(f"❌ Error during tab selection: {e}")
354
+ # Don't re-raise to prevent crash
@@ -0,0 +1,204 @@
1
+ """Reasoning Engines listing and management view."""
2
+ import tkinter as tk
3
+ from tkinter import ttk, messagebox
4
+ from typing import Callable
5
+
6
+ from ..core.api_client import ApiClient
7
+ from .ui_components import async_operation, StatusButton
8
+
9
+
10
+ class ReasoningEnginesView(ttk.Frame):
11
+ """UI for listing and managing reasoning engines."""
12
+
13
+ def __init__(self, master, api: ApiClient, log: Callable[[str], None]):
14
+ super().__init__(master)
15
+ self.api = api
16
+ self.log = log
17
+
18
+ # Cache authentication state to avoid repeated API calls
19
+ self._cached_auth_state = None
20
+ self._last_auth_check = 0
21
+ self._auth_cache_duration = 30 # 30 seconds
22
+
23
+ self._setup_ui()
24
+
25
+ def _setup_ui(self):
26
+ # Control buttons
27
+ btns = ttk.Frame(self)
28
+ btns.pack(fill="x", pady=(6, 4))
29
+
30
+ self.refresh_btn = StatusButton(btns, text="Refresh Engines", command=self.refresh)
31
+ self.refresh_btn.pack(side="left", padx=4)
32
+
33
+ self.delete_btn = StatusButton(btns, text="Delete Selected", command=self.delete_selected)
34
+ self.delete_btn.pack(side="left", padx=8)
35
+
36
+ self.status = tk.StringVar(value="Ready.")
37
+ ttk.Label(btns, textvariable=self.status).pack(side="right")
38
+
39
+ # Info label
40
+ info_frame = ttk.Frame(self)
41
+ info_frame.pack(fill="x", padx=4, pady=4)
42
+ ttk.Label(info_frame, text="💡 Reasoning Engines are the core compute units that power your agents.",
43
+ foreground="gray").pack(anchor="w")
44
+
45
+ # Engines list
46
+ wrap = ttk.Frame(self)
47
+ wrap.pack(fill="both", expand=True)
48
+ cols = ("id", "display_name", "create_time", "resource_name")
49
+ self.tree = ttk.Treeview(wrap, columns=cols, show="headings", selectmode="extended")
50
+
51
+ for c, t, w in [
52
+ ("id", "Engine ID", 200),
53
+ ("display_name", "Display Name", 300),
54
+ ("create_time", "Created", 180),
55
+ ("resource_name", "Full Resource Name", 500),
56
+ ]:
57
+ self.tree.heading(c, text=t)
58
+ self.tree.column(c, width=w, anchor="w")
59
+
60
+ self.tree.pack(side="left", fill="both", expand=True)
61
+ vsb = ttk.Scrollbar(wrap, orient="vertical", command=self.tree.yview)
62
+ self.tree.configure(yscroll=vsb.set)
63
+ vsb.pack(side="right", fill="y")
64
+
65
+ # Event bindings
66
+ self.tree.bind("<<TreeviewSelect>>", self._on_selection_change)
67
+ self.tree.bind("<Button-3>", self._popup)
68
+
69
+ # Context menu
70
+ self.menu = tk.Menu(self, tearoff=0)
71
+ self.menu.add_command(label="Delete", command=self.delete_selected)
72
+
73
+ self._update_button_states()
74
+
75
+ def _get_cached_auth_state(self) -> bool:
76
+ """Get authentication state with local caching to reduce API calls."""
77
+ import time
78
+ now = time.time()
79
+
80
+ # Use cached result if still fresh
81
+ if (self._cached_auth_state is not None and
82
+ (now - self._last_auth_check) < self._auth_cache_duration):
83
+ return self._cached_auth_state
84
+
85
+ # Check authentication and cache result
86
+ self._cached_auth_state = self.api.is_authenticated
87
+ self._last_auth_check = now
88
+ return self._cached_auth_state
89
+
90
+ def _update_button_states(self):
91
+ """Update button states based on current conditions."""
92
+ # Use cached authentication state to reduce API calls
93
+ is_auth = self._get_cached_auth_state()
94
+ self.refresh_btn.set_enabled(
95
+ is_auth,
96
+ "Authentication required" if not is_auth else ""
97
+ )
98
+
99
+ # Delete button - enabled only if engines are selected
100
+ has_selection = bool(self.tree.selection())
101
+ self.delete_btn.set_enabled(
102
+ has_selection,
103
+ "Select engines to delete" if not has_selection else ""
104
+ )
105
+
106
+ def _on_selection_change(self, event=None):
107
+ """Handle tree selection changes - IMMEDIATE update, no debouncing."""
108
+ # Immediate update - no timers or delays
109
+ self._update_button_states()
110
+
111
+ def refresh(self):
112
+ """Refresh the list of reasoning engines."""
113
+ # Use cached authentication state
114
+ if not self._get_cached_auth_state():
115
+ self.log("❌ Authentication required")
116
+ return
117
+
118
+ self.status.set("Loading…")
119
+ self.refresh_btn.set_enabled(False, "Loading...")
120
+
121
+ def callback(items):
122
+ self.refresh_btn.set_enabled(True)
123
+ for i in self.tree.get_children():
124
+ self.tree.delete(i)
125
+
126
+ if isinstance(items, Exception):
127
+ self.log(f"❌ List error: {items}")
128
+ self.status.set("Error")
129
+ return
130
+
131
+ for it in items:
132
+ # Format create time nicely
133
+ create_time = it.get("create_time", "Unknown")
134
+ if create_time != "Unknown" and "T" in create_time:
135
+ create_time = create_time.split("T")[0] # Just the date part
136
+
137
+ self.tree.insert("", "end", values=(
138
+ it["id"],
139
+ it["display_name"],
140
+ create_time,
141
+ it["resource_name"]
142
+ ))
143
+ self.status.set(f"{len(items)} engine(s)")
144
+ self._update_button_states()
145
+
146
+ async_operation(self.api.list_reasoning_engines, callback=callback, ui_widget=self)
147
+
148
+ def delete_selected(self):
149
+ """Delete selected reasoning engines."""
150
+ sel = self.tree.selection()
151
+ if not sel:
152
+ messagebox.showinfo("No selection", "Select one or more engines to delete.")
153
+ return
154
+
155
+ rows = [self.tree.item(i, "values") for i in sel]
156
+ resource_names = [r[3] for r in rows] # resource_name is column 3
157
+
158
+ if not messagebox.askyesno("Confirm delete",
159
+ f"Delete {len(resource_names)} selected engine(s)?\n\n"
160
+ "⚠️ This will permanently delete the reasoning engines!"):
161
+ return
162
+
163
+ self.status.set("Deleting…")
164
+ self.delete_btn.set_enabled(False, "Deleting...")
165
+
166
+ def ui_log(msg: str):
167
+ """Thread-safe logging - marshal to UI thread."""
168
+ self.after(0, lambda: self.log(msg))
169
+
170
+ def batch_delete():
171
+ success_count = 0
172
+ for resource_name in resource_names:
173
+ status, msg = self.api.delete_reasoning_engine_by_id(resource_name)
174
+ ui_log(f"{status.upper()}: {msg} — {resource_name}")
175
+ if status == "deleted":
176
+ success_count += 1
177
+ return success_count == len(resource_names)
178
+
179
+ def callback(success):
180
+ self.delete_btn.set_enabled(True)
181
+ self.refresh()
182
+ status_msg = "✅ Delete operation completed." if success else "⚠️ Delete completed with issues."
183
+ self.log(status_msg)
184
+
185
+ async_operation(batch_delete, callback=callback, ui_widget=self)
186
+
187
+ def _popup(self, event):
188
+ """Show context menu."""
189
+ row = self.tree.identify_row(event.y)
190
+ if row:
191
+ if row not in self.tree.selection():
192
+ self.tree.selection_set(row)
193
+ try:
194
+ self.menu.tk_popup(event.x_root, event.y_root)
195
+ finally:
196
+ self.menu.grab_release()
197
+
198
+ def update_api(self, api: ApiClient):
199
+ """Update the API client reference."""
200
+ self.api = api
201
+ # Clear cached auth state when API changes
202
+ self._cached_auth_state = None
203
+ self._last_auth_check = 0
204
+ self._update_button_states()