from __future__ import annotations from imgui_bundle import imgui import re import datetime from pathlib import Path from typing import TYPE_CHECKING, Any from src import imgui_scopes as imscope, theme_2 as theme, project_manager, mcp_client, ui_shared, markdown_helper if TYPE_CHECKING: from src.gui_2 import App def get_role_tint(role: str) -> imgui.ImVec4: """Returns a subtle background tint color based on the message role.""" # Tints: User(Blue), AI(Green), Vendor(Orange), System(Dark) if role == "User": return imgui.ImVec4(30/255, 45/255, 75/255, 0.5) elif role == "AI": return imgui.ImVec4(35/255, 65/255, 45/255, 0.5) elif role == "Vendor API": return imgui.ImVec4(65/255, 55/255, 35/255, 0.5) return imgui.ImVec4(20/255, 20/255, 20/255, 0.4) def render_thinking_trace(app: 'App', entry: dict, segments: list[dict], entry_index: int, is_standalone: bool = False) -> None: if not segments: return # Tint thinking trace background slightly differently with imscope.style_color(imgui.Col_.child_bg, imgui.ImVec4(40/255, 35/255, 25/255, 180/255)): with imscope.indent(): show_content = True if not is_standalone: header_label = f"Monologue ({len(segments)} traces)###thinking_header_{entry_index}" show_content = imgui.collapsing_header(header_label) if show_content: thinking_read_mode = entry.get("thinking_read_mode", True) if imgui.button(f"[Pure]##think_pure_{entry_index}" if thinking_read_mode else f"[Read]##think_read_{entry_index}"): entry["thinking_read_mode"] = not thinking_read_mode imgui.same_line() imgui.text_colored(ui_shared.C_TC, "Selectable toggle") h = 150 if is_standalone else 100 with imscope.child(f"thinking_content_{entry_index}", 0, h, True): for idx, seg in enumerate(segments): content = seg.get("content", "") marker = seg.get("marker", "thinking") with imscope.id(f"think_{entry_index}_{idx}"): imgui.text_colored(ui_shared.C_TC, f"[{marker}]") if thinking_read_mode: if app.ui_word_wrap: with imscope.text_wrap(imgui.get_content_region_avail().x): imgui.text(content) else: imgui.text(content) else: ui_shared.render_selectable_label(app, f"think_text_{entry_index}_{idx}", content, multiline=True, height=-1) imgui.separator() def render_discussion_entry(app: 'App', entry: dict, index: int) -> None: with imscope.id(f"disc_{index}"): role = entry.get("role", "User") bg_col = get_role_tint(role) draw_list = imgui.get_window_draw_list() p_min = imgui.get_cursor_screen_pos() full_width = imgui.get_content_region_avail().x # Start Background Layer draw_list.channels_split(2) draw_list.channels_set_current(1) # Foreground imgui.begin_group() # Force group to take full width to prevent squashing imgui.dummy(imgui.ImVec2(full_width, 0)) # Header controls collapsed, read_mode = entry.get("collapsed", False), entry.get("read_mode", False) if imgui.button("+" if collapsed else "-"): entry["collapsed"] = not collapsed imgui.same_line() ui_shared.render_text_viewer(app, f"Entry #{index+1}", entry["content"], id_suffix=f"disc_btn_{index}") imgui.same_line(); imgui.set_next_item_width(120) if imgui.begin_combo("##role", entry["role"]): for r in app.disc_roles: if imgui.selectable(r, r == entry["role"])[0]: entry["role"] = r imgui.end_combo() if not collapsed: imgui.same_line() if imgui.button("[Edit]" if read_mode else "[Read]"): entry["read_mode"] = not read_mode ts_str = entry.get("ts", "") usage = entry.get("usage", {}) if ts_str or usage: imgui.same_line() if ts_str: imgui.text_colored(ui_shared.C_SUB, str(ts_str)) if usage: inp, out, cache = usage.get("input_tokens", 0), usage.get("output_tokens", 0), usage.get("cache_read_input_tokens", 0) u_str = f" in:{inp} out:{out}" + (f" cache:{cache}" if cache else "") imgui.same_line(); imgui.text_colored(imgui.ImVec4(0.4, 0.6, 0.7, 1.0), u_str) if collapsed: imgui.same_line() if imgui.button("Ins"): app.disc_entries.insert(index, {"role": "User", "content": "", "collapsed": True, "ts": project_manager.now_ts()}) imgui.same_line() if imgui.button("Del"): if entry in app.disc_entries: app.disc_entries.remove(entry) draw_list.channels_merge() return imgui.same_line() if imgui.button("Branch"): app._branch_discussion(index) imgui.same_line(); preview = entry["content"].replace("\n", " ")[:60] if len(entry["content"]) > 60: preview += "..." imgui.text_colored(ui_shared.C_SUB, preview) else: # Body content - FORCE START ON NEW LINE imgui.dummy(imgui.ImVec2(0, 4)) imgui.set_cursor_pos_x(imgui.get_cursor_start_pos().x) thinking_segments, has_content = entry.get("thinking_segments", []), bool(entry.get("content", "").strip()) if thinking_segments: render_thinking_trace(app, entry, thinking_segments, index, is_standalone=not has_content) imgui.dummy(imgui.ImVec2(0, 4)) if read_mode: render_discussion_entry_read_mode(app, entry, index) else: if not (bool(thinking_segments) and not has_content): ch, entry["content"] = imgui.input_text_multiline("##content", entry["content"], imgui.ImVec2(-1, 150)) imgui.end_group() # 2. Draw Background Rectangle draw_list.channels_set_current(0) # Background p_max = imgui.get_item_rect_max() # Ensure full width coverage p_max.x = p_min.x + full_width + imgui.get_style().window_padding.x draw_list.add_rect_filled(p_min, p_max, imgui.get_color_u32(bg_col), 4.0) draw_list.channels_merge() imgui.separator() def render_discussion_entry_read_mode(app: 'App', entry: dict, index: int) -> None: with imscope.id(f"read_{index}"): content = entry["content"] if not content.strip(): return if '## Retrieved Context' in content: rag_match = re.search(r'## Retrieved Context\n\n([\s\S]*?)(?=\n\n#|\Z)', content) if rag_match: rag_section = rag_match.group(1) if imgui.collapsing_header('Retrieved Context'): chunks = re.finditer(r'### Chunk (\d+) \(Source: (.*?)\)\n([\s\S]*?)(?=\n### Chunk|\Z)', rag_section) for chunk_match in chunks: idx, path, chunk_content = chunk_match.group(1), chunk_match.group(2), chunk_match.group(3) if imgui.collapsing_header(f'Chunk {idx}: {path}'): if imgui.button(f'[Source]##rag_{index}_{idx}'): res = mcp_client.read_file(path) if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type = path, res, (Path(path).suffix.lstrip('.') if Path(path).suffix else 'text'); app.show_windows["Text Viewer"] = True imgui.text_unformatted(chunk_content) content = content[:rag_match.start()] + content[rag_match.end():] pattern = re.compile(r"\[Definition: (.*?) from (.*?) \(line (\d+)\)\](\s+```[\s\S]*?```)?") matches = list(pattern.finditer(content)) # FORCE A NEW GROUP with no extra constraints imgui.begin_group() with theme.ai_text_style(): if not matches: markdown_helper.render(content, context_id=f"disc_{index}") else: last_idx = 0 for m_idx, match in enumerate(matches): before = content[last_idx:match.start()] if before: markdown_helper.render(before, context_id=f"disc_{index}_b_{m_idx}") header_text, path, code_block = match.group(0).split("\n")[0].strip(), match.group(2), match.group(4) if imgui.collapsing_header(header_text): if imgui.button(f"[Source]##{index}_{match.start()}"): res = mcp_client.read_file(path) if res: app.text_viewer_title, app.text_viewer_content, app.text_viewer_type = path, res, (Path(path).suffix.lstrip(".") if Path(path).suffix else "text"); app.show_windows["Text Viewer"] = True if code_block: markdown_helper.render(code_block, context_id=f"disc_{index}_c_{m_idx}") last_idx = match.end() after = content[last_idx:] if after: markdown_helper.render(after, context_id=f"disc_{index}_a") imgui.end_group()