/
ncit
/
coderagsystem
Обзор
Документация
Войти
/
ncit
/
coderagsystem
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
codex/rag-codegraph-patterns
src/rag/tui/dashboard.py
897 строк
29 KB
ncit
Fix RAG indexing and CLI regressions
31 май 2026, 08:43
31 май 2026, 08:43
aa4ceec
Код
Авторство
О чём код?
"""RAG System TUI — redesigned dashboard. Layout follows the Pencil mock in ragsystem.pen: ┌──────────────────────────────────────────────────────────────────┐ │ • Home/Dashboard ragsys 2 of 8 14:32 │ status bar ├───────────┬──────────────────────────────────────────────────────┤ │ Home │ │ │ Search │ │ │ Ask │ <active screen content> │ │ Index │ │ │ Filters │ │ │ Logs │ │ │ Overview │ │ │ Help │ │ ├───────────┴──────────────────────────────────────────────────────┤ │ :cmd search payment processing ↵ │ cmd line └──────────────────────────────────────────────────────────────────┘ Colors: dark slate background, teal accent, violet for agent/model, blue for identifiers. JetBrains Mono / Geist Mono only. """ from __future__ import annotations from datetime import datetime from textual.app import ComposeResult from textual.binding import Binding from textual.containers import ( Horizontal, Vertical, VerticalScroll, Container, ) from textual.reactive import reactive from textual.screen import ModalScreen from textual.widgets import ( Button, Checkbox, Input, Label, ListItem, ListView, ProgressBar, RadioButton, RadioSet, RichLog, Static, ) # Reuse the existing helper widgets for sub-content; full dashboard frame is new. # --------------------------------------------------------------------------- # Top status bar # --------------------------------------------------------------------------- class StatusBar(Static): """Always-visible top status bar — traffic-light buttons + state pills. Shows: mac-style window controls, screen name, daemon dot, repo, embedder model, generator model, qpm, mem usage, clock. """ DEFAULT_CSS = """ StatusBar { dock: top; height: 1; padding: 0 1; background: #12161e; color: #5a6a7a; } """ daemon_ok = reactive(False) repo_name = reactive("(none)") embedder = reactive("?") gen_model = reactive("?") qpm = reactive(0.0) mem_mb = reactive(0) screen_name = reactive("Home") def render(self) -> str: lights = "[red]●[/red] [yellow]●[/yellow] [green]●[/green]" dot = "[green]●[/green]" if self.daemon_ok else "[red]●[/red]" clock = datetime.now().strftime("%H:%M") # Aggressive truncation to keep bar single-line at any reasonable width. screen_short = self.screen_name.split(" / ")[0][:12] embed_short = self.embedder.split(":")[0].replace("qwen3-embedding", "qwen3-emb")[:12] gen_short = self.gen_model[:10] repo_short = self.repo_name[:14] return ( f"{lights} [#5ee6d0 b]{screen_short}[/]" f" {dot}" f" [dim]repo[/dim] [#7fb6f0]{repo_short}[/]" f" [dim]emb[/dim] [#5ee6d0]{embed_short}[/]" f" [dim]gen[/dim] [#b084eb]{gen_short}[/]" f" [dim]qpm[/dim] {self.qpm:.0f}" f" [dim]mem[/dim] {self.mem_mb}M" f" [#5a6a7a]{clock}[/]" ) # --------------------------------------------------------------------------- # Left sidebar — 8-item nav # --------------------------------------------------------------------------- class Sidebar(Static): """Left nav. 8 items + section labels. Active row highlighted with a teal cursor block and brighter foreground.""" DEFAULT_CSS = """ Sidebar { dock: left; width: 24; height: 100%; background: #10161d; padding: 1 0; border-right: tall #1a1f2a; } """ NAV = [ ("home", "Home", "h"), ("search", "Search", "s"), ("ask", "Ask", "a"), ("index", "Index", "i"), ("filters", "Filters", "f"), ("overview", "Overview", "o"), ("logs", "Logs", "l"), ("help", "Help", "?"), ] active = reactive("home") def render(self) -> str: lines = ["", " [#5a6a7a b]NAVIGATION[/]", ""] for key, label, hot in self.NAV: if key == self.active: lines.append( f" [#5ee6d0 on #141b23] █ {label:<10}[/] [#5a6a7a]{hot}[/]" ) else: lines.append( f" [#c8d6e5]{label:<10}[/] [#5a6a7a]{hot}[/]" ) lines += [ "", " [#5a6a7a b]ACTIONS[/]", "", " [#c8d6e5]Palette[/] [#5a6a7a]⌘K[/]", " [#c8d6e5]Cmd line[/] [#5a6a7a]:[/]", " [#c8d6e5]Clear[/] [#5a6a7a]c[/]", " [#c8d6e5]Quit[/] [#5a6a7a]q[/]", "", " [#5a6a7a]──────────────[/]", " [#5a6a7a]rag[/] [#5ee6d0]» tui[/]", ] return "\n".join(lines) # --------------------------------------------------------------------------- # Bottom command line — REPL-style input # --------------------------------------------------------------------------- class CmdLine(Container): """Always-visible bottom command line. Type ``:search foo``, ``:ask "how does X work"``, ``:list is_singleton``, ``:index /abs/path``, ``:filter lang=kotlin``, ``:strategy hybrid``, ``:goto search`` from any screen. """ DEFAULT_CSS = """ CmdLine { dock: bottom; height: 3; background: #141b23; border-top: solid #1a1f2a; padding: 0 1; } CmdLine Label { width: 2; color: #5ee6d0; text-style: bold; padding: 0; height: 1; } CmdLine Input { border: none; background: #141b23; color: #c8d6e5; height: 1; padding: 0; width: 1fr; } CmdLine Input:focus { border: none; background: #1a2330; } CmdLine #cmd-hints { width: 32; padding: 0 1; color: #5a6a7a; height: 1; content-align: right middle; } """ def compose(self) -> ComposeResult: with Horizontal(): yield Label(":") yield Input( placeholder='try: search payment · ask how does X work · list is_singleton --lang kotlin · goto logs', id="cmd-input", ) yield Static( "[#5a6a7a]↵ run ⌘K palette ? help[/]", id="cmd-hints", ) # --------------------------------------------------------------------------- # Screen frames — each is a content panel switched into the main area # --------------------------------------------------------------------------- class ScreenBase(Container): """Common scaffolding for a screen — content slot only. Fixed-height layout. Every screen has the same outer padding so the active area starts at a predictable row across all screens. """ DEFAULT_CSS = """ ScreenBase { height: 1fr; padding: 1 1 0 1; } """ title = "Screen" def compose(self) -> ComposeResult: yield from self.compose_body() def compose_body(self) -> ComposeResult: yield Static("(no body)") # ---------- Home ---------- class PanelHeader(Static): """Header strip for a Home panel — title left, secondary text right. Uses panel2-tone background and a 1px bottom border to match the Pencil mock. Set `.title` and `.meta` via update_title()/update_meta(). """ DEFAULT_CSS = """ PanelHeader { height: 1; padding: 0 1; background: #12161e; color: #5a6a7a; } """ title_text = reactive("") meta_text = reactive("") def __init__(self, title: str = "", meta: str = "", **kwargs): super().__init__(**kwargs) self.title_text = title self.meta_text = meta def render(self) -> str: # Compose a single-line "title …………… meta" string. Use simple # padding so the meta hugs the right edge inside the panel. # Textual already wraps inside the widget width — we just keep it tight. return ( f"[#5a6a7a]┌─[/] [#c8d6e5 b]{self.title_text}[/]" f" [#5a6a7a]{self.meta_text}[/]" ) def update_meta(self, meta: str) -> None: self.meta_text = meta class HomeScreen(ScreenBase): title = "Home / Dashboard" DEFAULT_CSS = """ HomeScreen { padding: 0 1; } HomeScreen .home-kpis { height: 5; margin: 1 0 1 0; } HomeScreen .kpi { border: round #1a1f2a; padding: 0 1; width: 1fr; height: 5; margin: 0 1; background: #10161d; } HomeScreen .home-mid { height: 1fr; } HomeScreen .home-left { width: 1fr; height: 1fr; margin: 0 1; } HomeScreen .home-right { width: 32; height: 1fr; margin: 0 1; } HomeScreen .panel { border: round #1a1f2a; background: #10161d; height: 1fr; } HomeScreen .panel-body { padding: 1 1; height: 1fr; } HomeScreen #qpm-spark { height: 1; padding: 0 1; } HomeScreen #qpm-axis { height: 1; padding: 0 1; color: #5a6a7a; } HomeScreen #qpm-divider { height: 1; background: #1a1f2a; margin: 0 1; } HomeScreen #queries-title { height: 1; padding: 0 1; color: #5a6a7a; } HomeScreen #home-recent-queries { height: 1fr; margin: 0 1; background: transparent; } HomeScreen #home-recent-queries ListItem { height: 1; padding: 0; } HomeScreen .right-stack { height: 1fr; } HomeScreen #plugins-panel { height: 1fr; } HomeScreen #collections-panel { height: 1fr; margin-top: 1; } HomeScreen #home-plugins { height: 1fr; padding: 0 1; } HomeScreen #home-collections { height: 1fr; padding: 0 1; } """ def compose_body(self) -> ComposeResult: with Horizontal(classes="home-kpis"): yield Static( "[#5a6a7a]INDEX SIZE[/]\n[bold #5ee6d0]—[/]\n[dim]chunks[/dim]", classes="kpi", id="kpi-index", ) yield Static( "[#5a6a7a]EMBEDDER[/]\n[bold #5ee6d0]—[/]\n[dim]Ollama[/dim]", classes="kpi", id="kpi-embedder", ) yield Static( "[#5a6a7a]GENERATOR[/]\n[bold #b084eb]—[/]\n[dim]Ollama[/dim]", classes="kpi", id="kpi-gen", ) yield Static( "[#5a6a7a]UPTIME[/]\n[bold #7fd18b]—[/]\n[dim]daemon[/dim]", classes="kpi", id="kpi-uptime", ) with Horizontal(classes="home-mid"): with Vertical(classes="home-left"): with Vertical(classes="panel"): yield PanelHeader("QPM (24h)", "avg — · peak —", id="qpm-header") with Vertical(classes="panel-body"): yield Static( "[#1f2a35]" + "▁" * 48 + "[/]", id="qpm-spark", ) yield Static( "[#5a6a7a]00:00 06:00 12:00 18:00 now[/]", id="qpm-axis", ) yield Static("", id="qpm-divider") yield Static("[#5a6a7a]RECENT QUERIES[/]", id="queries-title") yield ListView(id="home-recent-queries") with Vertical(classes="home-right"): with Vertical(classes="panel", id="plugins-panel"): yield PanelHeader("PLUGINS", "", id="plugins-header") yield Static("[dim]loading...[/dim]", id="home-plugins") with Vertical(classes="panel", id="collections-panel"): yield PanelHeader("COLLECTIONS", "", id="collections-header") yield Static("[dim]loading...[/dim]", id="home-collections") # ---------- Search ---------- class SearchScreen(ScreenBase): title = "Search" DEFAULT_CSS = """ SearchScreen .search-input-row { height: 3; padding: 0 1; } SearchScreen #search-input { width: 1fr; } SearchScreen .search-mid { height: 1fr; } SearchScreen #search-results { width: 2fr; border: round $primary 30%; margin: 0 1; padding: 0 1; } SearchScreen #search-detail { width: 3fr; border: round $primary 30%; margin: 0 1; padding: 0 1; } SearchScreen #search-plan { height: 5; border: round $primary 30%; margin: 0 1; padding: 0 1; } """ def compose_body(self) -> ComposeResult: with Horizontal(classes="search-input-row"): yield Input( placeholder="Query — Enter to search (Ctrl+A toggles Ask mode)", id="search-input", ) with Horizontal(classes="search-mid"): yield ListView(id="search-results") yield RichLog(highlight=True, markup=True, id="search-detail", wrap=False) yield Static("[dim]Planner inspector will show after first query.[/dim]", id="search-plan") # ---------- Ask ---------- class AskScreen(ScreenBase): title = "Ask (RAG)" DEFAULT_CSS = """ AskScreen .ask-input-row { height: 3; padding: 0 1; } AskScreen #ask-input { width: 1fr; } AskScreen .ask-mid { height: 1fr; } AskScreen #ask-answer { width: 3fr; border: round $primary 30%; margin: 0 1; } AskScreen #ask-citations { width: 2fr; border: round $primary 30%; margin: 0 1; } AskScreen #ask-meta { height: 1; padding: 0 1; color: $text-muted; } """ def compose_body(self) -> ComposeResult: with Horizontal(classes="ask-input-row"): yield Input( placeholder="Ask a grounded question — Enter to run", id="ask-input", ) with Horizontal(classes="ask-mid"): yield RichLog(highlight=True, markup=True, id="ask-answer") yield ListView(id="ask-citations") yield Static("", id="ask-meta") # ---------- Index ---------- class IndexScreen(ScreenBase): title = "Index Manager" DEFAULT_CSS = """ IndexScreen .index-input-row { height: 3; padding: 0 1; } IndexScreen #index-path { width: 3fr; } IndexScreen #index-go { width: 1fr; } IndexScreen #index-status { height: 3; border: round $primary 30%; padding: 0 1; margin: 0 1; } IndexScreen #index-progress { height: 1; margin: 0 2; } IndexScreen .index-mid { height: 1fr; } IndexScreen #index-repos { width: 2fr; border: round $primary 30%; margin: 0 1; } IndexScreen #index-recent { width: 3fr; border: round $primary 30%; margin: 0 1; } """ def compose_body(self) -> ComposeResult: with Horizontal(classes="index-input-row"): yield Input(placeholder="/absolute/path/to/repo", id="index-path") yield Button("Index", id="index-go", variant="primary") yield Static("[dim]Idle. Pick a path above and press Index.[/dim]", id="index-status") yield ProgressBar(total=100, show_eta=False, id="index-progress") with Horizontal(classes="index-mid"): yield ListView(id="index-repos") yield ListView(id="index-recent") # ---------- Filters ---------- class FiltersScreen(ScreenBase): title = "Filters & Strategy" DEFAULT_CSS = """ FiltersScreen .filters-mid { height: 1fr; } FiltersScreen .filter-group { border: round $primary 30%; padding: 1 1; margin: 0 1; width: 1fr; height: auto; } FiltersScreen .group-title { color: $accent; } """ def compose_body(self) -> ComposeResult: with Horizontal(classes="filters-mid"): with Vertical(classes="filter-group"): yield Static("[bold]Language[/bold]", classes="group-title") yield Checkbox("kotlin", id="f-lang-kotlin") yield Checkbox("java", id="f-lang-java") yield Checkbox("python", id="f-lang-python") yield Checkbox("dart", id="f-lang-dart") yield Checkbox("typescript", id="f-lang-typescript") with Vertical(classes="filter-group"): yield Static("[bold]Concurrency[/bold]", classes="group-title") yield Checkbox("is_suspend", id="f-is-suspend") yield Checkbox("uses_coroutines", id="f-uses-coroutines") yield Checkbox("uses_flow", id="f-uses-flow") yield Checkbox("uses_async_java", id="f-uses-async-java") with Vertical(classes="filter-group"): yield Static("[bold]Pattern[/bold]", classes="group-title") yield Checkbox("is_singleton", id="f-is-singleton") yield Checkbox("is_data_class", id="f-is-data-class") yield Checkbox("is_sealed", id="f-is-sealed") yield Checkbox("is_interface", id="f-is-interface") yield Checkbox("is_composable", id="f-is-composable") yield Checkbox("is_di_component", id="f-is-di-component") with Vertical(classes="filter-group"): yield Static("[bold]Strategy override[/bold]", classes="group-title") with RadioSet(id="f-strategy"): yield RadioButton("auto (planner)", value=True, id="strat-auto") yield RadioButton("hybrid", id="strat-hybrid") yield RadioButton("filtered", id="strat-filtered") yield RadioButton("graph_walk", id="strat-graph_walk") yield RadioButton("aggregate", id="strat-aggregate") yield RadioButton("global", id="strat-global") yield RadioButton("naive", id="strat-naive") # ---------- Logs ---------- class LogsScreen(ScreenBase): title = "Logs Tail" DEFAULT_CSS = """ LogsScreen #logs-view { height: 1fr; border: round $primary 30%; margin: 0 1; } LogsScreen #logs-heatmap { height: 4; border: round $primary 30%; margin: 0 1; padding: 0 1; color: $text-muted; } """ def compose_body(self) -> ComposeResult: yield RichLog(highlight=True, markup=True, id="logs-view", wrap=False, max_lines=2000) yield Static( "[bold]24h event volume (5-min bins)[/bold]\n" "[cyan]▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅▄▄▃▃▂▂▁▁▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆▅▅[/cyan]\n" "[dim]00:00 12:00 23:59[/dim]", id="logs-heatmap", ) # ---------- Overview ---------- class OverviewScreen(ScreenBase): title = "Overview" DEFAULT_CSS = """ OverviewScreen .overview-mid { height: 1fr; } OverviewScreen .panel { border: round $primary 30%; padding: 1 1; margin: 0 1; height: 1fr; } """ def compose_body(self) -> ComposeResult: with Horizontal(classes="overview-mid"): with VerticalScroll(classes="panel"): yield Static("[bold]Module summaries[/bold]") yield Static("[dim]loading...[/dim]", id="ov-summaries") with VerticalScroll(classes="panel"): yield Static("[bold]Graph communities[/bold]") yield Static("[dim]loading...[/dim]", id="ov-communities") yield Static("") yield Static("[bold]Top connected nodes[/bold]") yield Static("[dim]loading...[/dim]", id="ov-nodes") # ---------- Help ---------- class HelpScreen(ScreenBase): title = "Help" DEFAULT_CSS = """ HelpScreen .help-grid { height: 1fr; } HelpScreen .panel { border: round $primary 30%; padding: 1 2; margin: 0 1; height: 1fr; } """ def compose_body(self) -> ComposeResult: with Horizontal(classes="help-grid"): with VerticalScroll(classes="panel"): yield Static("[bold]Hotkeys[/bold]\n") yield Static( " [cyan]q[/cyan] Quit\n" " [cyan]?[/cyan] Toggle Help screen\n" " [cyan]ctrl+k[/cyan] Command palette\n" " [cyan]:[/cyan] Focus command line\n" " [cyan]h[/cyan] Home\n" " [cyan]s[/cyan] Search\n" " [cyan]a[/cyan] Ask\n" " [cyan]i[/cyan] Index\n" " [cyan]f[/cyan] Filters\n" " [cyan]o[/cyan] Overview\n" " [cyan]l[/cyan] Logs\n" " [cyan]c[/cyan] Clear active log / results\n" ) with VerticalScroll(classes="panel"): yield Static("[bold]Active config[/bold]\n") yield Static("[dim]loading...[/dim]", id="help-config") yield Static("") yield Static("[bold]:cmd line examples[/bold]\n") yield Static( " [cyan]:search[/cyan] payment processing\n" " [cyan]:ask[/cyan] how does checkout work\n" " [cyan]:list[/cyan] is_singleton --lang kotlin\n" " [cyan]:index[/cyan] /path/to/repo\n" " [cyan]:filter[/cyan] lang=kotlin\n" " [cyan]:strategy[/cyan] hybrid\n" " [cyan]:goto[/cyan] logs\n" ) # --------------------------------------------------------------------------- # Command palette modal (⌘K) # --------------------------------------------------------------------------- class CommandPalette(ModalScreen): """Fuzzy-search modal for any verb in the TUI. Returns the selected command string via ``self.dismiss(cmd)``. """ BINDINGS = [Binding("escape", "dismiss", "Cancel")] DEFAULT_CSS = """ CommandPalette { align: center middle; } CommandPalette > Container { background: $surface; border: thick $accent; padding: 1 2; width: 80; height: 24; } CommandPalette Input { width: 1fr; margin-bottom: 1; } CommandPalette ListView { height: 1fr; } CommandPalette .pal-group { color: $accent; } """ COMMANDS = [ ("ACTIONS", [ ("search ...", "search"), ("ask ...", "ask"), ("list is_singleton", "list is_singleton"), ("list is_suspend", "list is_suspend"), ("list uses_coroutines", "list uses_coroutines"), ("index <path>", "index"), ("reload config", "reload"), ("clear log", "clear"), ]), ("NAVIGATE", [ ("go to Home", "goto home"), ("go to Search", "goto search"), ("go to Ask", "goto ask"), ("go to Index", "goto index"), ("go to Filters", "goto filters"), ("go to Overview", "goto overview"), ("go to Logs", "goto logs"), ("go to Help", "goto help"), ]), ("DAEMON", [ ("show /status", "status"), ("show /health detail", "health"), ("show recent events", "events"), ("show collections", "collections"), ("show plugins", "plugins"), ]), ] def compose(self) -> ComposeResult: with Container(): yield Static("[bold cyan]⌘ Command Palette[/bold cyan] [dim]· type to filter, ↵ run, esc cancel[/dim]") yield Input(placeholder="Type a command…", id="pal-input") yield ListView(id="pal-list") async def on_mount(self) -> None: await self._populate("") async def _populate(self, q: str) -> None: lv = self.query_one("#pal-list", ListView) await lv.clear() q_low = q.strip().lower() for group_name, items in self.COMMANDS: await lv.append(ListItem(Static(f"[dim]{group_name}[/dim]"))) for label, cmd in items: if q_low and q_low not in label.lower() and q_low not in cmd.lower(): continue item = ListItem(Static(f" [bold]{label}[/bold] [dim]→ {cmd}[/dim]")) item.command_value = cmd # type: ignore[attr-defined] await lv.append(item) async def on_input_changed(self, event: Input.Changed) -> None: if event.input.id == "pal-input": await self._populate(event.value) async def on_input_submitted(self, event: Input.Submitted) -> None: # Submit the first non-group item that matches lv = self.query_one("#pal-list", ListView) for item in lv.children: if isinstance(item, ListItem) and hasattr(item, "command_value"): self.dismiss(item.command_value) # type: ignore[attr-defined] return self.dismiss(None) async def on_list_view_selected(self, event: ListView.Selected) -> None: item = event.item if hasattr(item, "command_value"): self.dismiss(item.command_value) # type: ignore[attr-defined] # --------------------------------------------------------------------------- # Dashboard frame — composes everything # --------------------------------------------------------------------------- class Dashboard(Container): """Outer frame: status bar (top), sidebar (left), cmd line (bottom), screen swap area (center).""" DEFAULT_CSS = """ Dashboard { height: 1fr; } Dashboard #screen-area { height: 1fr; padding: 0 0; } """ SCREENS = { "home": HomeScreen, "search": SearchScreen, "ask": AskScreen, "index": IndexScreen, "filters": FiltersScreen, "overview": OverviewScreen, "logs": LogsScreen, "help": HelpScreen, } def __init__(self, host: str, port: int, initial_screen: str = "home", **kwargs): super().__init__(**kwargs) self._host = host self._port = port self._active = initial_screen if initial_screen in self.SCREENS else "home" def compose(self) -> ComposeResult: yield StatusBar(id="status-bar") yield Sidebar(id="sidebar") yield CmdLine(id="cmd-line") cls = self.SCREENS.get(self._active, HomeScreen) with Container(id="screen-area"): yield cls(id=f"screen-{self._active}") def on_mount(self) -> None: # Sync sidebar + status bar with the initial screen on first mount. try: self.query_one("#sidebar", Sidebar).active = self._active cls = self.SCREENS.get(self._active, HomeScreen) self.query_one("#status-bar", StatusBar).screen_name = cls.title except Exception: pass async def switch_screen(self, name: str) -> None: """Replace the screen-area contents with a new screen widget. Uses ``remove_children()`` to drop all children atomically — calling ``await child.remove()`` per-child while children hold focused Inputs caused NoActiveAppError on subsequent removals. """ if name not in self.SCREENS: return if name == self._active: return cls = self.SCREENS[name] try: area = self.query_one("#screen-area", Container) except Exception: return # Drop focus before removing — focused Input being yanked mid-remove # raises NoActiveAppError. try: self.app.set_focus(None) except Exception: pass try: await area.remove_children() except Exception: pass try: await area.mount(cls(id=f"screen-{name}")) except Exception: return self._active = name try: self.query_one("#sidebar", Sidebar).active = name self.query_one("#status-bar", StatusBar).screen_name = cls.title except Exception: pass