From 2f73aaa33398768534c37bc38b2352ceea5dd6e6 Mon Sep 17 00:00:00 2001 From: Nam Pham Dinh Thanh Date: Tue, 18 Aug 2026 16:30:45 +0900 Subject: [PATCH] Cowork's status strip: the drawn order, and figures that are there on arrival MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The drawing reads it left to right — Agent: qwen2.5-coder · Định tuyến: Tắt · ↓292.8K ↑102.7K · $0.31 · 📁 folder — and the app had it split down the middle: usage and folder on the left, Agent and routing away on the right. They are one run on the left now, in that order. Nén and Tự chạy stay on the right, where the control inventory marks them "giữ nguyên tại chỗ". The figures were also missing. _usage_total_lbl was written in exactly one place, at the end of a turn, so a thread opened from History showed an empty strip however much it had already spent — the numbers only appeared once you sent another message. refresh_usage() reads the same per-conversation events _show_usage does and now runs wherever the thread changes: opening one, starting a new one, or deriving a title from the first turn. Opening a seeded thread now reads ↓103.5k ↑44.6k ▤201.0k $0.1187 straight away. check_cowork_screen asserts the strip is non-empty for a thread with recorded usage and that the four parts run left to right in the drawn order. 23/23 checkers pass. Co-Authored-By: Claude Opus 5 (1M context) --- tools/check_cowork_screen.py | 22 +++++++++++++++++++ ui/chat_panel.py | 42 +++++++++++++++++++++++++++++++----- 2 files changed, 59 insertions(+), 5 deletions(-) diff --git a/tools/check_cowork_screen.py b/tools/check_cowork_screen.py index f0bb55e..bbc1ecf 100644 --- a/tools/check_cowork_screen.py +++ b/tools/check_cowork_screen.py @@ -87,6 +87,28 @@ def main() -> int: fails.append(f"thieu nut {name} tren thanh cong cu") print(f"nut tren thanh cong cu: {c.skills_btn.text()!r}, {c._new_btn.text()!r}") + # the status strip, read left to right, is + # Agent: … · Định tuyến: … · ↓in ↑out · $cost · 📁 folder + from PySide6.QtCore import QPoint as _P + + bar = c._usage_total_lbl.parentWidget() + def _x(widget): + return widget.mapTo(bar, _P(0, 0)).x() + + usage_text = c._usage_total_lbl.text() + print(f"usage tren dai: {usage_text!r}") + if not usage_text.strip(): + fails.append("dai duoi khong hien token/chi phi cua thread da mo") + order = [("Agent", _x(c._agent_lbl)), ("Dinh tuyen", _x(c.routing_toggle)), + ("usage", _x(c._usage_total_lbl))] + folder = getattr(c, "folder_lbl", None) + if folder is not None: + order.append(("thu muc", _x(folder))) + print("thu tu: " + " < ".join(f"{n}({x})" for n, x in order)) + for (n1, x1), (n2, x2) in zip(order, order[1:]): + if x1 >= x2: + fails.append(f"dai duoi sai thu tu: {n1} khong dung truoc {n2}") + # the drawing gives Cowork two columns; History lives in the rail's RECENTS # and its pane arrives folded to the strip the drawing keeps w = win.workspace diff --git a/ui/chat_panel.py b/ui/chat_panel.py index e19bb86..c578f10 100644 --- a/ui/chat_panel.py +++ b/ui/chat_panel.py @@ -143,7 +143,7 @@ class ChatPanel(QWidget): self._usage_total_lbl = QLabel("") self._usage_total_lbl.setObjectName("hint") self._usage_total_lbl.setStyleSheet(f"color: {current_palette().text_faint};") - self.composer.add_bottom_left(self._usage_total_lbl) + # Per-tab "Agent" = which MODEL this tab uses (e.g. deepseek / gemma / # qwen for the local provider). Cowork and Code pick independently and @@ -167,13 +167,19 @@ class ChatPanel(QWidget): self.agent_combo.setMinimumWidth(150) self.agent_combo.setToolTip(tr("chatpanel.agent_tooltip")) self.agent_combo.currentIndexChanged.connect(self._on_agent_changed) - self.composer.add_bottom_right(self._agent_lbl) - self.composer.add_bottom_right(self.agent_combo) + self.composer.add_bottom_left(self._agent_lbl) + self.composer.add_bottom_left(self.agent_combo) # Off/Auto/Manual routing toggle — lets the router pick the best-fit # model per message (see core/routing + _apply_routing). from .routing_toggle import RoutingToggle self.routing_toggle = RoutingToggle(ctx, self.kind) - self.composer.add_bottom_right(self.routing_toggle) + # The drawing reads the strip left to right as + # Agent: … · Định tuyến: … · ↓in ↑out · $cost · 📁 folder + # so these sit together on the left, with the folder box the Cowork tab + # appends landing after them. Nén and Tự chạy stay on the right, where + # the control inventory marks them "giữ nguyên tại chỗ". + self.composer.add_bottom_left(self.routing_toggle) + self.composer.add_bottom_left(self._usage_total_lbl) # Manual "compress conversation" — trim old history to cut tokens. self.compress_btn = QPushButton(tr("chatpanel.compress_btn")) self.compress_btn.setIcon(app_icon("compress")) @@ -1413,6 +1419,26 @@ class ChatPanel(QWidget): return [e for e in ut.load_events() if e.get("source") == self.kind and e.get("label") == label] + def refresh_usage(self) -> None: + """Show what this conversation has already cost. + + The label was written only at the end of a turn, so opening a thread + from History left the strip blank however much it had spent. + """ + from ..core import model_pricing as mp + from ..core import usage_tracker as ut + + cur = self._usage_snapshot() + if not (cur["in"] or cur["out"] or cur["cache"]): + self._usage_total_lbl.setText("") + return + # same source _show_usage reads, so the two never disagree + pricing = {**ut.DEFAULT_PRICING, **(self.ctx.config.data.get("usage") or {})} + self._usage_total_lbl.setText( + f"↓{mp.format_tokens(cur['in'])} ↑{mp.format_tokens(cur['out'])} " + f"▤{mp.format_tokens(cur['in'] + cur['out'] + cur['cache'])} " + f"{ut.format_cost(self._session_cost_usd(), pricing)}") + def _usage_snapshot(self) -> Dict[str, int]: """Cumulative in/out/cache tokens for THIS conversation so far.""" snap = {"in": 0, "out": 0, "cache": 0} @@ -1684,10 +1710,16 @@ class ChatPanel(QWidget): self.history_changed.emit() # current view changed → refresh History highlight def _notify_title(self) -> None: - """Let a screen that heads itself with the thread title follow along.""" + """Let a screen that heads itself with the thread title follow along. + + The thread also decides what the usage strip should read, so refresh + that here rather than at each of the three places the title changes. + """ hook = getattr(self, "refresh_title", None) if callable(hook): hook() + if getattr(self, "_usage_total_lbl", None) is not None: + self.refresh_usage() def load_conversation(self, conv: Dict[str, Any]) -> None: """Switch the view to a stored conversation. Allowed while work is running —