diff --git a/app.py b/app.py index a9bb76e..c42017a 100644 --- a/app.py +++ b/app.py @@ -10,8 +10,9 @@ from PySide6.QtCore import Qt, QTimer from PySide6.QtGui import QGuiApplication, QIcon from PySide6.QtWidgets import ( QApplication, QComboBox, QHBoxLayout, QLabel, QMainWindow, QMenu, - QPushButton, QSizePolicy, QSplitter, QStackedWidget, QSystemTrayIcon, - QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, + QPushButton, QScrollArea, QSizePolicy, QSplitter, QStackedWidget, + QSystemTrayIcon, QToolButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget, ) from . import APP_NAME, DISPLAY_NAME, __version__ @@ -238,17 +239,36 @@ class MainWindow(QMainWindow): self.workspace.projects_changed.connect(self._sync_rail_project) self._syncing_rail_project = False self._sync_rail_project() - nvl.addWidget(self.nav, 0) + # The destinations and RECENTS scroll together; the bottom group, the + # Settings button and the account row stay pinned below them. + # + # Without this the rail simply ran out of room on a short window (a + # 1280×720 laptop leaves ~570px here): nav and the bottom group have + # fixed heights, so the squeeze fell entirely on RECENTS, and once that + # hit zero the layout drew the "GẦN ĐÂY" heading straight over the last + # nav row. + self._nav_scroll = QScrollArea() + self._nav_scroll.setObjectName("navScroll") + self._nav_scroll.setWidgetResizable(True) + self._nav_scroll.setFrameShape(QScrollArea.NoFrame) + self._nav_scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + scroll_body = QWidget() + sv = QVBoxLayout(scroll_body) + sv.setContentsMargins(0, 0, 0, 0) + sv.setSpacing(0) + sv.addWidget(self.nav, 0) # RECENTS — the threads of the project named in the picker above, right # where Claude puts them. A shortcut only: the full History panel (search, # filters, pin, bulk delete, context menu) stays exactly where it is, and # "all projects…" at the end of this list opens it. self.nav_recents_hdr = QLabel(tr("app.nav.recents")) self.nav_recents_hdr.setObjectName("navSectionHdr") - nvl.addWidget(self.nav_recents_hdr) + sv.addWidget(self.nav_recents_hdr) self.nav_recents = self._new_nav_tree("navRecents") self.nav_recents.itemClicked.connect(self._on_rail_recent) - nvl.addWidget(self.nav_recents, 1) + sv.addWidget(self.nav_recents, 1) + self._nav_scroll.setWidget(scroll_body) + nvl.addWidget(self._nav_scroll, 1) # Bottom-pinned group: the places you visit occasionally, kept out of the # way of the ones you live in. A hairline (styled via #navrailBottom in # theme.py) separates the two lists. diff --git a/theme.py b/theme.py index 8f488b5..a7131f6 100644 --- a/theme.py +++ b/theme.py @@ -377,6 +377,19 @@ QWidget#navWrap QTreeWidget::item:disabled { color: $text_faint; } /* The pinned bottom group (Dashboard, Monitoring) — one hairline separates it from the list above so "occasional" reads apart from "everyday". */ QTreeWidget#navrailBottom { border-top: 1px solid $nav_border; } +QScrollArea#navScroll { background: transparent; border: none; } +QScrollArea#navScroll > QWidget > QWidget { background: transparent; } +/* Monitoring ▸ Overview reads as titled sections down one column, the way the + audit page draws it — a quiet caps heading with the content flat underneath, + not six bordered boxes competing with the cards inside them. */ +QGroupBox#monSection { + background: transparent; border: none; margin-top: 16px; + padding: 6px 0 0 0; font-weight: 700; +} +QGroupBox#monSection::title { + subcontrol-origin: margin; subcontrol-position: top left; left: 2px; top: 0; + padding: 0; color: $text_faint; font-size: 11px; letter-spacing: 0.5px; +} /* Segmented control: two-to-four choices shown side by side (language, theme) instead of a drop-list you must open to see what the options even are. */ QPushButton#segItem { diff --git a/tools/check_layout_geometry.py b/tools/check_layout_geometry.py index 9df01e3..22ebe5d 100644 --- a/tools/check_layout_geometry.py +++ b/tools/check_layout_geometry.py @@ -24,9 +24,11 @@ from capture_screens import _freeze_schedulers, _isolate_home, _load_fonts # no # The rail, top to bottom, as the audit page's rail() helper draws it. RAIL_ORDER = ["Project", "Cowork", "Co4E", "Thư mục", "GraphRAG", "Schedule Task"] RAIL_BOTTOM = ["Dashboard", "Giám sát"] -# Monitoring ▸ Tổng quan, as the wireframe stacks it. -MON_ORDER = ["ov_usage_group", "ov_activity_group", "ov_resource_group", - "ov_sandbox_details_group", "ov_pricing_group", "ov_audit_group"] +# Monitoring ▸ Tổng quan, in the order the wireframe stacks it: what it cost → +# what the machine is doing → what the agent may touch → per-model prices → +# what actually happened. +MON_ORDER = ["ov_usage_group", "ov_resource_group", "ov_sandbox_details_group", + "ov_pricing_group", "ov_activity_group", "ov_audit_group"] def main() -> int: diff --git a/ui/monitoring_tab.py b/ui/monitoring_tab.py index b61d41c..6961b62 100644 --- a/ui/monitoring_tab.py +++ b/ui/monitoring_tab.py @@ -417,6 +417,7 @@ class MonitoringTab(QWidget): # ---- Token Usage & Cost -------------------------------------------- self.ov_usage_group = QGroupBox() + self.ov_usage_group.setObjectName("monSection") usage_lay = QGridLayout(self.ov_usage_group) usage_lay.setSpacing(8) self.ov_usage_total = StatCard() @@ -447,41 +448,54 @@ class MonitoringTab(QWidget): # ---- Recent Activity ---------------------------------------------- self.ov_activity_group = QGroupBox() + self.ov_activity_group.setObjectName("monSection") act_lay = QVBoxLayout(self.ov_activity_group) self.ov_activity_lbl = QLabel() self.ov_activity_lbl.setWordWrap(True) self.ov_activity_lbl.setTextFormat(Qt.RichText) act_lay.addWidget(self.ov_activity_lbl) - left.addWidget(self.ov_activity_group) + # added near the bottom, beside the audit log — see below # ---- Resource Usage ------------------------------------------------- self.ov_resource_group = QGroupBox() - res_lay = QVBoxLayout(self.ov_resource_group) + self.ov_resource_group.setObjectName("monSection") + res_lay = QHBoxLayout(self.ov_resource_group) + res_lay.setSpacing(18) def _bar_row(): - row = QHBoxLayout() - lbl = QLabel(); lbl.setFixedWidth(70) - bar = QProgressBar(); bar.setFixedHeight(8); bar.setTextVisible(False) - val = QLabel(); val.setFixedWidth(90); val.setAlignment(Qt.AlignRight) - row.addWidget(lbl); row.addWidget(bar, 1); row.addWidget(val) - res_lay.addLayout(row) + # One metric as a compact column: name, value, and the gauge under + # them. The bars stay — the wireframe writes this section as a + # single line, and dropping them would lose the at-a-glance load. + cell = QVBoxLayout() + cell.setSpacing(2) + top = QHBoxLayout() + lbl = QLabel() + val = QLabel(); val.setAlignment(Qt.AlignRight) + top.addWidget(lbl); top.addStretch(1); top.addWidget(val) + bar = QProgressBar(); bar.setFixedHeight(6); bar.setTextVisible(False) + cell.addLayout(top); cell.addWidget(bar) + res_lay.addLayout(cell, 1) return lbl, bar, val def _text_row(): + cell = QVBoxLayout() + cell.setSpacing(2) row = QHBoxLayout() lbl = QLabel(); val = QLabel(); val.setAlignment(Qt.AlignRight) row.addWidget(lbl); row.addStretch(1); row.addWidget(val) - res_lay.addLayout(row) + cell.addLayout(row) + cell.addSpacing(6) + res_lay.addLayout(cell, 1) return lbl, val self.ov_cpu_lbl, self.ov_cpu_bar, self.ov_cpu_val = _bar_row() self.ov_mem_lbl, self.ov_mem_bar, self.ov_mem_val = _bar_row() self.ov_disk_lbl, self.ov_disk_val = _text_row() self.ov_network_lbl, self.ov_network_val = _text_row() - res_lay.addStretch(1) # ---- Model pricing (beside the CPU/resource group) ------------------ self.ov_pricing_group = QGroupBox() + self.ov_pricing_group.setObjectName("monSection") pg = QVBoxLayout(self.ov_pricing_group) phdr = QHBoxLayout() self.ov_pricing_ccy_lbl = QLabel(); self.ov_pricing_ccy_lbl.setObjectName("hint") @@ -524,6 +538,7 @@ class MonitoringTab(QWidget): # ---- Sandbox Details -------------------------------------------- self.ov_sandbox_details_group = QGroupBox() + self.ov_sandbox_details_group.setObjectName("monSection") sbx_lay = QVBoxLayout(self.ov_sandbox_details_group) def _kv(): @@ -561,6 +576,7 @@ class MonitoringTab(QWidget): # ---- Permissions ----------------------------------------------- self.ov_permissions_group = QGroupBox() + self.ov_permissions_group.setObjectName("monSection") perm_lay = QVBoxLayout(self.ov_permissions_group) def _pkv(): @@ -584,8 +600,12 @@ class MonitoringTab(QWidget): # ---- Model pricing — its own section, full width ------------------ root.addWidget(self.ov_pricing_group) + # ---- What actually happened, last --------------------------------- + root.addWidget(self.ov_activity_group) + # ---- Audit Log ---------------------------------------------------- self.ov_audit_group = QGroupBox() + self.ov_audit_group.setObjectName("monSection") audit_lay = QVBoxLayout(self.ov_audit_group) self.ov_audit_lbl = QLabel() self.ov_audit_lbl.setWordWrap(True) @@ -645,12 +665,12 @@ class MonitoringTab(QWidget): # A QGroupBox title treats "&" as a mnemonic marker, so "token & Chi # phí" rendered as "token _Chi phí". Double it to show a literal "&". self.ov_usage_group.setTitle( - tr("monitoring.overview_usage_title").replace("&", "&&")) + tr("monitoring.overview_usage_title").upper().replace("&", "&&")) self.ov_budget_card.apply_btn.setToolTip(tr("usage.budget_apply_tooltip")) self.ov_budget_card.budget_spin.setToolTip(tr("usage.budget_spin_tooltip")) - self.ov_activity_group.setTitle(tr("monitoring.overview_activity_title")) - self.ov_resource_group.setTitle(tr("monitoring.overview_resource_title")) + self.ov_activity_group.setTitle(tr("monitoring.overview_activity_title").upper()) + self.ov_resource_group.setTitle(tr("monitoring.overview_resource_title").upper()) self.security_page.filter_edit.setPlaceholderText(tr("monitoring.filter_placeholder")) self.action_page.filter_edit.setPlaceholderText(tr("monitoring.filter_placeholder")) self.ov_cpu_lbl.setText(tr("monitoring.overview_res_cpu")) @@ -658,7 +678,7 @@ class MonitoringTab(QWidget): self.ov_disk_lbl.setText(tr("monitoring.overview_res_disk")) self.ov_network_lbl.setText(tr("monitoring.overview_res_network")) # model pricing panel - self.ov_pricing_group.setTitle(tr("monitoring.pricing_title")) + self.ov_pricing_group.setTitle(tr("monitoring.pricing_title").upper()) self.ov_pricing_ccy_lbl.setText(tr("monitoring.pricing_currency")) self.ov_price_import_btn.setText(tr("monitoring.pricing_import")) self.ov_price_export_btn.setText(tr("monitoring.pricing_export")) @@ -670,7 +690,7 @@ class MonitoringTab(QWidget): tr("monitoring.pricing_col_maxout"), tr("monitoring.pricing_col_input"), tr("monitoring.pricing_col_output")]) - self.ov_sandbox_details_group.setTitle(tr("monitoring.overview_sandbox_details_title")) + self.ov_sandbox_details_group.setTitle(tr("monitoring.overview_sandbox_details_title").upper()) self.ov_sbx_id_lbl.setText(tr("monitoring.overview_sandbox_id")) self.ov_sbx_status_lbl.setText(tr("monitoring.overview_status")) self.ov_sbx_created_lbl.setText(tr("monitoring.overview_created")) @@ -678,7 +698,7 @@ class MonitoringTab(QWidget): self.ov_sbx_edit_btn.setText(tr("monitoring.overview_edit")) self.ov_sbx_net_lbl.setText(tr("monitoring.overview_network_label")) - self.ov_permissions_group.setTitle(tr("monitoring.overview_permissions_title")) + self.ov_permissions_group.setTitle(tr("monitoring.overview_permissions_title").upper()) self.ov_perm_fs_lbl.setText(tr("monitoring.overview_perm_fs")) self.ov_perm_fs_val.setText(tr("monitoring.overview_perm_fs_value")) self.ov_perm_network_lbl.setText(tr("monitoring.overview_perm_network")) @@ -688,7 +708,7 @@ class MonitoringTab(QWidget): self.ov_perm_env_val.setText(tr("monitoring.overview_perm_env_value")) self.ov_perm_edit_btn.setText(tr("monitoring.overview_edit")) - self.ov_audit_group.setTitle(tr("monitoring.overview_audit_title")) + self.ov_audit_group.setTitle(tr("monitoring.overview_audit_title").upper()) self.ov_view_all_btn.setText(tr("monitoring.overview_view_all")) self.refresh()