diff --git a/i18n.py b/i18n.py index 0425cff..c3b91c3 100644 --- a/i18n.py +++ b/i18n.py @@ -2383,6 +2383,11 @@ STRINGS: Dict[str, Dict[str, str]] = { "monitoring.col_role": {"en": "Agent Role", "ja": "エージェント役割", "vi": "Vai trò Agent"}, "monitoring.col_name": {"en": "Action", "ja": "アクション", "vi": "Hành động"}, "monitoring.col_result": {"en": "Result", "ja": "結果", "vi": "Kết quả"}, + # Security Events shows WHICH rule fired instead of a result that is always + # the same — every security_block is recorded with ok=False. + "monitoring.col_action": {"en": "Action", "ja": "アクション", "vi": "Hành động"}, + # The fourth KPI tile on Overview, as the wireframe labels it. + "monitoring.overview_calls": {"en": "Calls", "ja": "呼び出し", "vi": "Lượt gọi"}, "monitoring.col_detail": {"en": "Detail", "ja": "詳細", "vi": "Chi tiết"}, "monitoring.col_account": {"en": "Account", "ja": "アカウント", "vi": "Tài khoản"}, "monitoring.col_machine": {"en": "Machine", "ja": "マシン", "vi": "Máy"}, diff --git a/ui/monitoring_tab.py b/ui/monitoring_tab.py index 6961b62..3f716f2 100644 --- a/ui/monitoring_tab.py +++ b/ui/monitoring_tab.py @@ -74,24 +74,41 @@ class _EventTable(QTableWidget): every column header is click-to-sort (ascending/descending toggle; the Time column's ISO timestamps sort correctly as text).""" - def __init__(self): - super().__init__(0, 7) + # What each blocked action is, as a colour. Security events all record + # ok=False, so the tick/cross column said the same thing on every row; the + # useful distinction is WHICH rule fired. + _ACTION_TINTS = { + "prompt": "accent", + "dangerous_command": "danger", + "run_command": "danger", + "install_package": "warning", + "path_outside_sandbox": "success", + "network_blocked": "accent", + "secret_in_output": "warning", + } + + def __init__(self, show_result: bool = True): + # Security Events drops the result column entirely (see _ACTION_TINTS). + self._show_result = show_result + super().__init__(0, 7 if show_result else 6) self.setEditTriggers(QTableWidget.NoEditTriggers) self.setSelectionBehavior(QTableWidget.SelectRows) self.verticalHeader().setVisible(False) self.setSortingEnabled(True) header = self.horizontalHeader() header.setStretchLastSection(True) - for col in range(6): + for col in range(self.columnCount() - 1): header.setSectionResizeMode(col, QHeaderView.ResizeToContents) def retranslate(self) -> None: - self.setHorizontalHeaderLabels([ - tr("monitoring.col_time"), tr("monitoring.col_role"), - tr("monitoring.col_account"), tr("monitoring.col_machine"), - tr("monitoring.col_name"), tr("monitoring.col_result"), - tr("monitoring.col_detail"), - ]) + cols = [tr("monitoring.col_time"), tr("monitoring.col_role"), + tr("monitoring.col_account"), tr("monitoring.col_machine")] + if self._show_result: + cols += [tr("monitoring.col_name"), tr("monitoring.col_result")] + else: + cols += [tr("monitoring.col_action")] + cols += [tr("monitoring.col_detail")] + self.setHorizontalHeaderLabels(cols) def set_events(self, events: List[dict]) -> None: events = sorted(events, key=lambda e: e.get("ts", ""), reverse=True)[:_MAX_ROWS] @@ -102,14 +119,28 @@ class _EventTable(QTableWidget): cells = [ ev.get("ts", ""), agent_roles.label_for(ev.get("agent_role", "")), ev.get("account", "") or "—", ev.get("machine", "") or "—", - ev.get("name", ""), "", - (ev.get("detail") or "")[:300], + ev.get("name", ""), ] + if self._show_result: + cells.append("") + cells.append((ev.get("detail") or "")[:300]) + pal = current_palette() for col, text in enumerate(cells): item = QTableWidgetItem(str(text)) - if col == 5: # Result — green check / red close icon (no emoji) + if self._show_result and col == 5: + # Result — green check / red close icon (no emoji) item.setIcon(icon("check", color=DOT_GREEN) if ev.get("ok") else icon("close", color=DOT_RED)) + if not self._show_result and col == 4: + # The action itself, tinted by which rule fired — this is + # the column the always-identical result column made way for. + tint = getattr(pal, self._ACTION_TINTS.get( + ev.get("name", ""), "text_muted"), pal.text_muted) + colour = QColor(tint) + item.setForeground(QBrush(colour)) + soft = QColor(colour) + soft.setAlpha(38) + item.setBackground(QBrush(soft)) if is_admin_violation: item.setBackground(QBrush(QColor(229, 72, 77, 60))) self.setItem(row, col, item) @@ -159,7 +190,9 @@ class MonitoringTab(QWidget): self.tabs.addTab(self._build_overview_page(), "") visible = self._tab_visible - self.security_table = _EventTable() + # Security events are always ok=False, so this table trades the + # tick/cross column for a tinted Action column (see _EventTable). + self.security_table = _EventTable(show_result=False) self.security_page = self._wrap_with_filter(self.security_table) if visible("security_events"): self.tabs.addTab(self.security_page, "") @@ -425,19 +458,24 @@ class MonitoringTab(QWidget): self.ov_usage_out = StatCard() self.ov_usage_cache = StatCard() self.ov_usage_cost = StatCard() - for i, card in enumerate((self.ov_usage_total, self.ov_usage_in, self.ov_usage_out, - self.ov_usage_cache, self.ov_usage_cost)): + self.ov_usage_calls = StatCard() + for i, card in enumerate((self.ov_usage_total, self.ov_usage_cost, + self.ov_usage_calls)): usage_lay.addWidget(card, 0, i) + # Kept alive and updated, but off the KPI row: their figures ride on the + # token tile's sub-line instead of taking three tiles of their own. + for hidden in (self.ov_usage_in, self.ov_usage_out, self.ov_usage_cache): + hidden.setVisible(False) # Budget: remaining/budget, direct entry, auto-warns red past 85% used — # same box (and same usage.budget_* config) as the Dashboard's. self.ov_budget_card = BudgetCard() self.ov_budget_card.apply_btn.setIcon(icon("check")) self.ov_budget_card.apply_btn.clicked.connect(self._apply_budget) - usage_lay.addWidget(self.ov_budget_card, 0, 5, 2, 1) + usage_lay.addWidget(self.ov_budget_card, 0, 3) # Equal stretch on every column — otherwise the grid sizes each column # to its widest cell's natural content (Budget's longer "$X / $Y" value # + entry row makes its column wider than the plain stat cards). - for col in range(6): + for col in range(4): usage_lay.setColumnStretch(col, 1) # Unit prices are NOT entered here anymore — the cost total is computed @@ -844,8 +882,12 @@ class MonitoringTab(QWidget): events = ut.load_events() s = ut.summarize(events) costs = ut.cost_usd_events(events, pricing) - self.ov_usage_total.set(tr("dashboard.card_total"), fmt_tokens(s["total"]), - tr("dashboard.card_turns", n=s["turns"])) + self.ov_usage_total.set( + tr("dashboard.card_total"), fmt_tokens(s["total"]), + f'{tr("dashboard.card_in")} {fmt_tokens(s["in"])} · ' + f'{tr("dashboard.card_out")} {fmt_tokens(s["out"])} · ' + f'{tr("dashboard.card_cache")} {fmt_tokens(s["cache"])}') + self.ov_usage_calls.set(tr("monitoring.overview_calls"), str(s["turns"]), "") self.ov_usage_in.set(tr("dashboard.card_in"), fmt_tokens(s["in"]), ut.format_cost(costs["in"], pricing)) self.ov_usage_out.set(tr("dashboard.card_out"), fmt_tokens(s["out"]),