From 3e67c3e4bfa8f91cc676ce208dacb042f30f3f7f Mon Sep 17 00:00:00 2001 From: Suntion <149924916+SunYanbox@users.noreply.github.com> Date: Tue, 5 May 2026 17:20:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(console):=20=E9=87=8D=E6=9E=84=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E9=9D=A2=E6=9D=BF=E4=B8=BA=E6=A0=87=E7=AD=BE=E9=A1=B5?= =?UTF-8?q?=E5=B8=83=E5=B1=80=20(#145)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增功能: 引入 TabbedContent 组件将原有垂直堆叠的统计信息拆分为独立标签页 * 添加 `TabbedContent` 和 `TabPane` 导入,创建 "Overview", "Session", "Tools", "Sessions" 四个标签 * 将原有的 `mount(Static)`、`mount(Label)` 和 `mount(DataTable)` 调用迁移至对应的 `TabPane` 实例中 * 在 `_build_content` 末尾统一挂载包含所有分区的 `TabbedContent` 容器 - 修复问题: 优化页面重建时的用户交互体验 * 在 `on_mount` 或重建逻辑前保存当前激活的标签页状态 (`tc.active`) * 内容重建完成后尝试恢复之前选中的标签页索引,防止切换后焦点丢失 - 重构优化: 调整样式定义与代码结构 * 新增针对 `StatsTab TabbedContent` 和 `StatsTab TabPane` 的 CSS 样式,设置高度为 `1fr` 并启用 `overflow-y: auto` * 更新注释标识从 "Section" 改为 "Tab",明确各部分对应独立的标签页 --- src/console/ui/widgets/stats_tab.py | 69 ++++++++++++++++++++++------- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/src/console/ui/widgets/stats_tab.py b/src/console/ui/widgets/stats_tab.py index 3cef052..d428450 100644 --- a/src/console/ui/widgets/stats_tab.py +++ b/src/console/ui/widgets/stats_tab.py @@ -6,7 +6,7 @@ from textual.containers import Horizontal, Vertical from textual.screen import ModalScreen -from textual.widgets import Button, DataTable, Input, Label, Static +from textual.widgets import Button, DataTable, Input, Label, Static, TabbedContent, TabPane class RenameDialog(ModalScreen[str | None]): @@ -135,7 +135,18 @@ class StatsTab(Vertical): height: 1fr; width: 1fr; padding: 0 1; + } + + StatsTab TabbedContent { + height: 1fr; + width: 1fr; + } + + StatsTab TabPane { + height: 1fr; + width: 1fr; overflow-y: auto; + padding: 0 1; } #stats-placeholder { @@ -265,11 +276,27 @@ async def _refresh(self) -> None: if self._db is None: return + # Save active tab before rebuilding + active_tab = "" + try: + tc = self.query_one(TabbedContent) + active_tab = tc.active + except Exception: + pass + await self.remove_children() self._build_content() + # Restore active tab + if active_tab: + try: + tc = self.query_one(TabbedContent) + tc.active = active_tab + except Exception: + pass + def _build_content(self) -> None: - """Mount all content widgets.""" + """Mount all content widgets into tabbed layout.""" if self._db is None: return @@ -297,7 +324,7 @@ def _build_content(self) -> None: if row: current_name = row[0] - # --- Section 1: Overview --- + # --- Tab 1: Overview --- overview_text = ( f"[bold]Overview[/bold]\n" f"Total Sessions: {total_sessions}\n" @@ -305,14 +332,16 @@ def _build_content(self) -> None: f"Overall Success Rate: {success_rate:.1f}%\n" f"Active Session: {current_name or 'N/A'}" ) - self.mount(Static(overview_text, id="stats-overview")) + overview_pane = TabPane("Overview", id="tab-overview") + overview_pane.mount(Static(overview_text, id="stats-overview")) - # --- Section 2: Current session stats --- + # --- Tab 2: Current Session --- + session_pane = TabPane("Session", id="tab-session") if self._current_session_id is not None: summary = self._db.get_session_summary(self._current_session_id) if summary: duration_str = self._format_duration(summary["duration"]) - self.mount(Label("Current Session", classes="stats-header")) + session_pane.mount(Label("Current Session", classes="stats-header")) dt = DataTable(id="stats-current-session") dt.add_columns("Metric", "Value") dt.add_row("Duration", duration_str) @@ -320,23 +349,27 @@ def _build_content(self) -> None: dt.add_row("Successful", str(summary["success_count"])) dt.add_row("Failed", str(summary["fail_count"])) dt.add_row("Success Rate", f"{summary['success_rate']:.1f}%") - self.mount(dt) + session_pane.mount(dt) + else: + session_pane.mount(Label("No active session.", id="stats-empty")) - # --- Section 3: Tool usage ranking --- + # --- Tab 3: Tool Ranking --- + tools_pane = TabPane("Tools", id="tab-tools") ranking = self._db.get_tool_usage_ranking(self._current_session_id) if ranking: - self.mount(Label("Top Tools", classes="stats-header")) + tools_pane.mount(Label("Top Tools", classes="stats-header")) dt = DataTable(id="stats-tool-ranking") dt.add_columns("#", "Tool", "Calls", "Avg Time", "Total Time") for i, (func_name, count, avg_dur, total_dur) in enumerate(ranking, 1): avg_str = f"{avg_dur:.1f}ms" if avg_dur is not None else "N/A" total_str = f"{total_dur:.1f}ms" if total_dur is not None else "N/A" dt.add_row(str(i), func_name, str(count), avg_str, total_str) - self.mount(dt) + tools_pane.mount(dt) else: - self.mount(Label("No tool calls recorded yet.", id="stats-empty")) + tools_pane.mount(Label("No tool calls recorded yet.", id="stats-empty")) - # --- Section 4: Session list --- + # --- Tab 4: Sessions List --- + sessions_pane = TabPane("Sessions", id="tab-sessions") if sessions: total_pages = (len(sessions) + self._sessions_per_page - 1) // self._sessions_per_page if self._session_page >= total_pages: @@ -348,7 +381,7 @@ def _build_content(self) -> None: end_idx = start_idx + self._sessions_per_page page_sessions = sessions[start_idx:end_idx] - self.mount(Label("Sessions", classes="stats-header")) + sessions_pane.mount(Label("Sessions", classes="stats-header")) if total_pages > 1: nav = Horizontal( @@ -360,7 +393,7 @@ def _build_content(self) -> None: id="stats-pagination", classes="stats-pagination", ) - self.mount(nav) + sessions_pane.mount(nav) for s in page_sessions: sid, name, _created_at, duration = s @@ -383,9 +416,15 @@ def _build_content(self) -> None: Button("Delete", id=f"delete-{sid}", variant="error"), classes="stats-session-row", ) - self.mount(row) + sessions_pane.mount(row) if is_active: row.query_one(f"#delete-{sid}", Button).disabled = True + else: + sessions_pane.mount(Label("No sessions yet.", id="stats-empty")) + + # Mount the tabbed content + tc = TabbedContent(overview_pane, session_pane, tools_pane, sessions_pane) + self.mount(tc) async def on_button_pressed(self, event: Button.Pressed) -> None: """Handle rename/delete button clicks."""