From 4498ec501e4991e9c49358e6509d360c408752ce Mon Sep 17 00:00:00 2001 From: "xinzhu.yin" Date: Thu, 28 May 2026 17:34:51 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=A7=E7=BB=AD=E4=BC=98=E5=8C=96AI=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E5=88=97=E8=A1=A8UI=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/panels/ai_image_panel.py | 559 +++++++++++++++++++++-------- 1 file changed, 417 insertions(+), 142 deletions(-) diff --git a/app/views/panels/ai_image_panel.py b/app/views/panels/ai_image_panel.py index 58a2ef2..8977181 100644 --- a/app/views/panels/ai_image_panel.py +++ b/app/views/panels/ai_image_panel.py @@ -5,6 +5,8 @@ from __future__ import annotations import os import sys import threading +import time +import logging import tkinter as tk from tkinter import filedialog, messagebox, simpledialog @@ -20,6 +22,9 @@ if TYPE_CHECKING: from pqAutomationApp import PQAutomationApp +logger = logging.getLogger(__name__) + + def _theme_colors(): style = ttk.Style() colors = style.colors @@ -35,6 +40,118 @@ def _theme_colors(): } +def _apply_ai_image_list_style(self: "PQAutomationApp"): + """刷新 AI 图片列表控件样式,尽量贴合当前主题色板。""" + palette = _theme_colors() + style = ttk.Style() + style.configure( + "AIImage.Treeview", + background=palette["input_bg"], + fieldbackground=palette["input_bg"], + foreground=palette["input_fg"], + bordercolor=palette["border"], + lightcolor=palette["border"], + darkcolor=palette["border"], + rowheight=24, + font=("微软雅黑", 9), + ) + style.configure( + "AIImage.Treeview.Heading", + background=palette["bg"], + foreground=palette["muted"], + bordercolor=palette["border"], + font=("微软雅黑", 9, "bold"), + ) + style.map( + "AIImage.Treeview", + background=[("selected", palette["select_bg"])], + foreground=[("selected", palette["select_fg"])], + ) + + +def _hide_tree_tooltip(self: "PQAutomationApp"): + tip = getattr(self, "_ai_image_tooltip", None) + if tip is None: + return + try: + tip.withdraw() + except Exception: + pass + self._ai_image_tooltip_item = "" + + +def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root: int, item_id: str): + if not text: + _hide_tree_tooltip(self) + return + tip = getattr(self, "_ai_image_tooltip", None) + if tip is None: + tip = tk.Toplevel(self.root) + tip.withdraw() + tip.overrideredirect(True) + tip.transient(self.root) + label = tk.Label( + tip, + text="", + justify=tk.LEFT, + anchor=tk.W, + bg="#ffffff", + fg="#1f2937", + relief=tk.SOLID, + bd=1, + padx=8, + pady=6, + font=("微软雅黑", 9), + wraplength=520, + ) + label.pack(fill=tk.BOTH, expand=True) + self._ai_image_tooltip = tip + self._ai_image_tooltip_label = label + else: + label = getattr(self, "_ai_image_tooltip_label", None) + if label is None: + return + + self._ai_image_tooltip_item = item_id + label.configure(text=text) + tip.geometry(f"+{x_root + 14}+{y_root + 18}") + tip.deiconify() + tip.lift() + + +def _on_tree_motion(self: "PQAutomationApp", event): + tree = self.ai_image_tree + item_id = tree.identify_row(event.y) + col = tree.identify_column(event.x) + region = tree.identify("region", event.x, event.y) + if not item_id or col != "#0" or region not in {"tree", "cell"}: + _hide_tree_tooltip(self) + return + + node_map = getattr(self, "_ai_image_node_map", None) or {} + ridx = node_map.get(item_id) + if ridx is None or ridx < 0 or ridx >= len(self.ai_image_records): + _hide_tree_tooltip(self) + return + + rec = self.ai_image_records[ridx] + full_text = (rec.display_name or "").strip() + if not full_text: + _hide_tree_tooltip(self) + return + + # 悬浮到任意图片标题项时都显示完整描述,避免因截断判定误差导致无响应。 + if getattr(self, "_ai_image_tooltip_item", "") == item_id: + tip = getattr(self, "_ai_image_tooltip", None) + if tip is not None: + try: + tip.geometry(f"+{event.x_root + 14}+{event.y_root + 18}") + except Exception: + pass + return + _show_tree_tooltip(self, full_text, event.x_root, event.y_root, item_id) + + @@ -56,6 +173,17 @@ def create_ai_image_panel(self: "PQAutomationApp"): self._ai_image_cancel_event = None self._ai_image_request_seq = 0 self._ai_image_active_seq = 0 + self._ai_image_node_map = {} + self._ai_image_session_node_map = {} + self._ai_image_all_records = [] + self._ai_image_search_var = tk.StringVar(value="") + self._ai_image_count_var = tk.StringVar(value="0 张图片") + self._ai_image_reloading = False + self._ai_image_select_guard = False + self._ai_image_list_loaded = False + self._ai_image_tooltip = None + self._ai_image_tooltip_label = None + self._ai_image_tooltip_item = "" container = ttk.Frame(frame, padding=10) container.pack(fill=tk.BOTH, expand=True) @@ -71,35 +199,65 @@ def create_ai_image_panel(self: "PQAutomationApp"): left.grid(row=0, column=0, sticky=tk.NS, padx=(0, 10)) left.grid_propagate(False) - ttk.Label(left, text="历史图片", font=("微软雅黑", 10, "bold")).pack( - anchor=tk.W, pady=(0, 4) + title_row = ttk.Frame(left) + title_row.pack(fill=tk.X, pady=(0, 4)) + ttk.Label(title_row, text="历史图片", font=("微软雅黑", 10, "bold")).pack(side=tk.LEFT) + ttk.Label( + title_row, + textvariable=self._ai_image_count_var, + foreground=palette["muted"], + font=("微软雅黑", 9), + ).pack(side=tk.RIGHT) + + search_row = ttk.Frame(left) + search_row.pack(fill=tk.X, pady=(0, 6)) + ttk.Label( + search_row, + text="搜索", + foreground=palette["muted"], + font=("微软雅黑", 9), + ).pack(side=tk.LEFT, padx=(0, 6)) + self.ai_image_search_entry = ttk.Entry( + search_row, + textvariable=self._ai_image_search_var, + bootstyle="secondary", ) + self.ai_image_search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) + self.ai_image_search_entry.bind("", lambda e: reload_ai_image_list(self, auto_select_first=False)) + ttk.Button( + search_row, + text="清空", + width=6, + bootstyle="secondary-outline", + command=lambda: _clear_list_search(self), + ).pack(side=tk.LEFT, padx=(6, 0)) list_wrap = ttk.Frame(left, padding=2) list_wrap.pack(fill=tk.BOTH, expand=True) + list_wrap.columnconfigure(0, weight=1) + list_wrap.rowconfigure(0, weight=1) + _apply_ai_image_list_style(self) scroll = ttk.Scrollbar(list_wrap, orient=tk.VERTICAL) - self.ai_image_listbox = tk.Listbox( + self.ai_image_tree = ttk.Treeview( list_wrap, - width=34, - height=1, # 由 pack fill/expand 撑满,height 仅为最小保底 - activestyle="none", - font=("微软雅黑", 9), - bd=1, - relief=tk.FLAT, - highlightthickness=1, - bg=palette["input_bg"], - fg=palette["input_fg"], - highlightbackground=palette["border"], - highlightcolor=palette["select_bg"], - selectbackground=palette["select_bg"], - selectforeground=palette["select_fg"], + columns=("time",), + show="tree headings", + selectmode="browse", + style="AIImage.Treeview", yscrollcommand=scroll.set, ) - scroll.config(command=self.ai_image_listbox.yview) - self.ai_image_listbox.pack(side=tk.LEFT, fill=tk.Y) - scroll.pack(side=tk.RIGHT, fill=tk.Y) - self.ai_image_listbox.bind("<>", lambda e: _on_list_select(self)) + self.ai_image_tree.heading("#0", text="标题") + self.ai_image_tree.heading("time", text="时间") + self.ai_image_tree.column("#0", width=210, minwidth=140, anchor=tk.W) + self.ai_image_tree.column("time", width=105, minwidth=90, stretch=False, anchor=tk.W) + scroll.config(command=self.ai_image_tree.yview) + self.ai_image_tree.grid(row=0, column=0, sticky=tk.NSEW) + scroll.grid(row=0, column=1, sticky=tk.NS) + self.ai_image_tree.bind("<>", lambda e: _on_list_select(self)) + self.ai_image_tree.bind("", lambda e: _on_tree_double_click(self, e)) + self.ai_image_tree.bind("", lambda e: _on_tree_motion(self, e)) + self.ai_image_tree.bind("", lambda e: _hide_tree_tooltip(self)) # 右键菜单:发送到 UCD / 重命名 / 另存为 / 删除 # 索引: 0=发送, 1=sep, 2=重命名, 3=另存为, 4=删除 self.ai_image_menu = tk.Menu(self.root, tearoff=0) @@ -120,7 +278,7 @@ def create_ai_image_panel(self: "PQAutomationApp"): label="删除", command=lambda: _delete_current(self), ) - self.ai_image_listbox.bind( + self.ai_image_tree.bind( "", lambda e: _show_list_context_menu(self, e), ) @@ -216,14 +374,17 @@ def create_ai_image_panel(self: "PQAutomationApp"): # 注册面板 self.register_panel("ai_image", frame, None, "ai_image_visible") self.ai_image_visible = False - - # 初次加载缓存 - reload_ai_image_list(self) + self.ai_image_meta_var.set("首次打开 AI 图片面板时加载缓存") def toggle_ai_image_panel(self: "PQAutomationApp"): """切换 AI 图片面板显隐。""" self.show_panel("ai_image") + _apply_ai_image_list_style(self) + if not getattr(self, "_ai_image_list_loaded", False): + logger.info("[AIImagePanel] 首次显示面板,开始加载列表") + reload_ai_image_list(self) + self._ai_image_list_loaded = True def _get_app_base_dir(self: "PQAutomationApp") -> str: @@ -245,58 +406,102 @@ def reload_ai_image_list(self: "PQAutomationApp", auto_select_first=True): 其下列出该轮生成的所有图片。会话按"最近使用"倒序,组内按时间倒序。 auto_select_first: 是否自动选中第一张图片(默认 True)。 """ - palette = _theme_colors() - self.ai_image_records = _svc.list_records(base_dir=_get_app_base_dir(self)) - self.ai_image_listbox.delete(0, tk.END) - # 维护行号 → 记录索引的映射;分隔头处为 None - self._ai_image_row_map = [] - self._ai_image_row_session_map = [] - sessions = _svc.group_records_by_session(self.ai_image_records) - flat = [] - current_sid = _svc.get_session_id() - for idx, sess in enumerate(sessions, start=1): - sid = sess["session_id"] - is_current = sid and sid == current_sid - header = _format_session_header(idx, sess, is_current=is_current) - self.ai_image_listbox.insert(tk.END, header) - # 头部行:禁用选中(视觉上变灰) - last = self.ai_image_listbox.size() - 1 - self.ai_image_listbox.itemconfig( - last, foreground=palette["muted"], selectforeground=palette["muted"], - background=palette["bg"], selectbackground=palette["bg"], + t0 = time.monotonic() + if getattr(self, "_ai_image_reloading", False): + logger.debug("[AIImagePanel] 忽略重入 reload 请求") + return + self._ai_image_reloading = True + try: + _apply_ai_image_list_style(self) + all_records = _svc.list_records(base_dir=_get_app_base_dir(self)) + self._ai_image_all_records = list(all_records) + self.ai_image_tree.delete(*self.ai_image_tree.get_children()) + self._ai_image_node_map = {} + self._ai_image_session_node_map = {} + + keyword = (self._ai_image_search_var.get() or "").strip().lower() + records = [] + for rec in all_records: + if not keyword: + records.append(rec) + continue + hay = "\n".join([ + rec.display_name or "", + rec.prompt or "", + os.path.basename(rec.image_path), + ]).lower() + if keyword in hay: + records.append(rec) + + self.ai_image_records = records + sessions = _svc.group_records_by_session(self.ai_image_records) + flat = [] + current_sid = _svc.get_session_id() + for idx, sess in enumerate(sessions, start=1): + sid = sess["session_id"] + is_current = sid and sid == current_sid + header = _format_session_header(idx, sess, is_current=is_current) + parent_id = self.ai_image_tree.insert( + "", + tk.END, + text=header, + values=("",), + open=True, + ) + self._ai_image_session_node_map[parent_id] = sid + for rec in sess["records"]: + label = _format_list_label(rec) + created = (rec.created_at or "").replace("T", " ")[:16] + node_id = self.ai_image_tree.insert( + parent_id, + tk.END, + text=label, + values=(created,), + ) + self._ai_image_node_map[node_id] = len(flat) + flat.append(rec) + # 替换为按显示顺序展平后的列表,便于其它逻辑(前一张/后一张等) + self.ai_image_records = flat + total_count = len(all_records) + visible_count = len(self.ai_image_records) + if keyword: + self._ai_image_count_var.set(f"{visible_count}/{total_count} 张") + else: + self._ai_image_count_var.set(f"{visible_count} 张图片") + if self.ai_image_records and auto_select_first: + first_id = "" + for parent in self.ai_image_tree.get_children(""): + children = self.ai_image_tree.get_children(parent) + if children: + first_id = children[0] + break + if first_id: + _set_tree_selection(self, first_id) + else: + self.ai_image_current = None + self.ai_image_photo = None + self.ai_image_canvas.delete("all") + self.ai_image_meta_var.set("暂无缓存图片" if not keyword else "当前筛选无结果") + logger.info( + "[AIImagePanel] reload 完成 total=%d visible=%d sessions=%d keyword=%r elapsed=%.3fs", + total_count, + visible_count, + len(sessions), + keyword, + time.monotonic() - t0, ) - self._ai_image_row_map.append(None) - self._ai_image_row_session_map.append(sid) - for rec in sess["records"]: - label = " " + _format_list_label(rec) - self.ai_image_listbox.insert(tk.END, label) - self._ai_image_row_map.append(len(flat)) - self._ai_image_row_session_map.append(rec.session_id) - flat.append(rec) - # 替换为按显示顺序展平后的列表,便于其它逻辑(前一张/后一张等) - self.ai_image_records = flat - if self.ai_image_records and auto_select_first: - # 选中第一张实际记录 - for row, ridx in enumerate(self._ai_image_row_map): - if ridx is not None: - self.ai_image_listbox.selection_clear(0, tk.END) - self.ai_image_listbox.selection_set(row) - self.ai_image_listbox.activate(row) - _select_record(self, self.ai_image_records[ridx]) - break - else: - self.ai_image_current = None - self.ai_image_photo = None - self.ai_image_canvas.delete("all") - self.ai_image_meta_var.set("暂无缓存图片") + self._ai_image_list_loaded = True + finally: + self._ai_image_reloading = False def _format_session_header(index: int, sess: dict, is_current: bool) -> str: started = (sess.get("started_at") or "").replace("T", " ")[:16] tag = "(当前)" if is_current else "" + count = len(sess.get("records") or []) if sess.get("session_id"): - return f"── 会话 #{index} · {started} {tag}──" - return f"── 未归类 · {started} ──" + return f"会话 #{index} · {started} · {count} 张 {tag}".strip() + return f"未归类 · {started} · {count} 张" def _format_list_label(rec: _svc.AIImageRecord) -> str: @@ -305,42 +510,105 @@ def _format_list_label(rec: _svc.AIImageRecord) -> str: extra = rec.extra or {} if isinstance(extra, dict) and extra.get("size"): size_tag = f"[{extra['size']}] " - else: - try: - from PIL import Image as _Im - with _Im.open(rec.image_path) as _im: - size_tag = f"[{_im.width}×{_im.height}] " - except Exception: - pass name_line = rec.display_name.splitlines()[0] if rec.display_name else "(未命名)" - # 列表宽度 width=34,需要扣除两格缩进 + size_tag - max_name = 34 - 2 - len(size_tag) - 2 + # Treeview 标题列可变宽,给展示名预留较长空间 + max_name = 40 - len(size_tag) if max_name > 4 and len(name_line) > max_name: name_line = name_line[:max_name] + "…" return f"{size_tag}{name_line}" +def _clear_list_search(self: "PQAutomationApp"): + if (self._ai_image_search_var.get() or "").strip() == "": + return + self._ai_image_search_var.set("") + reload_ai_image_list(self, auto_select_first=False) + _hide_tree_tooltip(self) + + +def _set_tree_selection(self: "PQAutomationApp", item_id: str): + if not item_id: + return + _hide_tree_tooltip(self) + try: + current = self.ai_image_tree.selection() + if current and current[0] == item_id: + node_map = getattr(self, "_ai_image_node_map", None) or {} + ridx = node_map.get(item_id) + if ridx is not None and 0 <= ridx < len(self.ai_image_records): + _select_record(self, self.ai_image_records[ridx]) + return + self.ai_image_tree.selection_set(item_id) + self.ai_image_tree.focus(item_id) + self.ai_image_tree.see(item_id) + except Exception: + return + node_map = getattr(self, "_ai_image_node_map", None) or {} + ridx = node_map.get(item_id) + if ridx is not None and 0 <= ridx < len(self.ai_image_records): + _select_record(self, self.ai_image_records[ridx]) + + +def _find_tree_item_by_record_id(self: "PQAutomationApp", record_id: str) -> str: + if not record_id: + return "" + node_map = getattr(self, "_ai_image_node_map", None) or {} + for item_id, ridx in node_map.items(): + if ridx is None or ridx >= len(self.ai_image_records): + continue + rec = self.ai_image_records[ridx] + if rec.id == record_id: + return item_id + return "" + + def _on_list_select(self: "PQAutomationApp"): - sel = self.ai_image_listbox.curselection() + if getattr(self, "_ai_image_reloading", False): + return + if getattr(self, "_ai_image_select_guard", False): + logger.debug("[AIImagePanel] 忽略重入选择事件") + return + sel = self.ai_image_tree.selection() if not sel: return - row = sel[0] - row_map = getattr(self, "_ai_image_row_map", None) or [] - if row >= len(row_map): + self._ai_image_select_guard = True + try: + item_id = sel[0] + node_map = getattr(self, "_ai_image_node_map", None) or {} + ridx = node_map.get(item_id) + if ridx is None: + session_id = _session_id_for_item(self, item_id) + if session_id: + logger.info("[AIImagePanel] 选中会话头 sid=%s", session_id[:8]) + _switch_to_session(self, session_id, show_message=False, refresh_list=False) + return + if 0 <= ridx < len(self.ai_image_records): + rec = self.ai_image_records[ridx] + if rec.session_id: + _switch_to_session( + self, + rec.session_id, + show_message=False, + refresh_list=False, + ) + _select_record(self, rec) + logger.debug("[AIImagePanel] 选中图片 id=%s sid=%s", rec.id, (rec.session_id or "")[:8]) + finally: + self._ai_image_select_guard = False + + +def _on_tree_double_click(self: "PQAutomationApp", event): + """双击会话头时只切换会话并展开;双击记录保持默认行为。""" + item_id = self.ai_image_tree.identify_row(event.y) + if not item_id: return - ridx = row_map[row] - if ridx is None: - session_id = _session_id_for_row(self, row) - if session_id: - _switch_to_session(self, session_id, show_message=False) - self.ai_image_listbox.selection_clear(row) + node_map = getattr(self, "_ai_image_node_map", None) or {} + if item_id in node_map: return - if 0 <= ridx < len(self.ai_image_records): - rec = self.ai_image_records[ridx] - if rec.session_id: - _switch_to_session(self, rec.session_id, show_message=False, target_record_id=rec.id) - _select_record(self, rec) + sid = _session_id_for_item(self, item_id) + if sid: + _switch_to_session(self, sid, show_message=False, refresh_list=False) def _select_record(self: "PQAutomationApp", rec: _svc.AIImageRecord): @@ -387,36 +655,54 @@ def _start_new_session(self: "PQAutomationApp"): return _svc.reset_session() self.ai_image_status_var.set("已开启新对话") + # 新对话创建后不要自动选中历史图片,否则会立即把 session 切回旧会话。 reload_ai_image_list(self, auto_select_first=False) + try: + self.ai_image_tree.selection_remove(self.ai_image_tree.selection()) + except Exception: + pass + self.ai_image_current = None + self.ai_image_photo = None + self.ai_image_canvas.delete("all") + self.ai_image_meta_var.set("新对话已开启,等待生成图片") -def _session_id_for_row(self: "PQAutomationApp", row: int) -> str: - session_map = getattr(self, "_ai_image_row_session_map", None) or [] - if row < 0 or row >= len(session_map): - return "" - return session_map[row] or "" +def _session_id_for_item(self: "PQAutomationApp", item_id: str) -> str: + session_map = getattr(self, "_ai_image_session_node_map", None) or {} + parent = item_id + if parent not in session_map: + try: + parent = self.ai_image_tree.parent(item_id) + except Exception: + parent = "" + return (session_map.get(parent) or "") if parent else "" -def _switch_to_session(self: "PQAutomationApp", session_id: str, show_message: bool = True, target_record_id: str = ""): +def _switch_to_session( + self: "PQAutomationApp", + session_id: str, + show_message: bool = True, + target_record_id: str = "", + refresh_list: bool = True, +): sid = (session_id or "").strip() if not sid: return if sid == _svc.get_session_id(): return _svc.set_session_id(sid) - reload_ai_image_list(self) + logger.info( + "[AIImagePanel] 切换会话 sid=%s refresh=%s target=%s", + sid[:8], + refresh_list, + target_record_id[:8] if target_record_id else "", + ) + if refresh_list: + reload_ai_image_list(self) if target_record_id: - for row, ridx in enumerate(getattr(self, "_ai_image_row_map", []) or []): - if ridx is None: - continue - rec = self.ai_image_records[ridx] - if rec.id == target_record_id: - self.ai_image_listbox.selection_clear(0, tk.END) - self.ai_image_listbox.selection_set(row) - self.ai_image_listbox.activate(row) - self.ai_image_listbox.see(row) - _select_record(self, rec) - break + item_id = _find_tree_item_by_record_id(self, target_record_id) + if item_id: + _set_tree_selection(self, item_id) self.ai_image_status_var.set("已切换到历史对话") if show_message: messagebox.showinfo("提示", "已切换到所选历史对话") @@ -512,18 +798,12 @@ def _on_request_done(self: "PQAutomationApp", record, exc, req_seq): self.ai_image_status_var.set("完成") self.ai_image_input.delete("1.0", tk.END) reload_ai_image_list(self) + if record is not None: + logger.info("[AIImagePanel] 生成完成并入列 id=%s sid=%s", record.id, (record.session_id or "")[:8]) if record is not None and self.ai_image_records: - for row, ridx in enumerate(getattr(self, "_ai_image_row_map", []) or []): - if ridx is None: - continue - r = self.ai_image_records[ridx] - if r.id == record.id: - self.ai_image_listbox.selection_clear(0, tk.END) - self.ai_image_listbox.selection_set(row) - self.ai_image_listbox.activate(row) - self.ai_image_listbox.see(row) - _select_record(self, r) - break + item_id = _find_tree_item_by_record_id(self, record.id) + if item_id: + _set_tree_selection(self, item_id) def _stop_request(self: "PQAutomationApp"): @@ -596,18 +876,9 @@ def _rename_current(self: "PQAutomationApp"): target_id = rec.id reload_ai_image_list(self) - # 重新定位 - for row, ridx in enumerate(getattr(self, "_ai_image_row_map", []) or []): - if ridx is None: - continue - r = self.ai_image_records[ridx] - if r.id == target_id: - self.ai_image_listbox.selection_clear(0, tk.END) - self.ai_image_listbox.selection_set(row) - self.ai_image_listbox.activate(row) - self.ai_image_listbox.see(row) - _select_record(self, r) - break + item_id = _find_tree_item_by_record_id(self, target_id) + if item_id: + _set_tree_selection(self, item_id) # ---------------- 发送到 UCD ---------------- @@ -615,17 +886,21 @@ def _rename_current(self: "PQAutomationApp"): def _show_list_context_menu(self: "PQAutomationApp", event): """在图片列表上显示右键菜单,并根据状态启用/禁用项。""" + _hide_tree_tooltip(self) try: - row = self.ai_image_listbox.nearest(event.y) + item_id = self.ai_image_tree.identify_row(event.y) except Exception: - row = -1 - row_map = getattr(self, "_ai_image_row_map", None) or [] - ridx = row_map[row] if 0 <= row < len(row_map) else None + item_id = "" + + node_map = getattr(self, "_ai_image_node_map", None) or {} + ridx = node_map.get(item_id) if ridx is not None and 0 <= ridx < len(self.ai_image_records): - self.ai_image_listbox.selection_clear(0, tk.END) - self.ai_image_listbox.selection_set(row) - self.ai_image_listbox.activate(row) + _set_tree_selection(self, item_id) _select_record(self, self.ai_image_records[ridx]) + elif item_id: + sid = _session_id_for_item(self, item_id) + if sid: + _switch_to_session(self, sid, show_message=False, refresh_list=False) has_selection = self.ai_image_current is not None ucd = getattr(self, "ucd", None) @@ -777,7 +1052,7 @@ class AIImagePanelMixin: _select_record = _select_record _redraw_preview = _redraw_preview _start_new_session = _start_new_session - _session_id_for_row = _session_id_for_row + _session_id_for_item = _session_id_for_item _switch_to_session = _switch_to_session _update_request_progress = _update_request_progress _send_prompt = _send_prompt