diff --git a/tools/check_cowork_screen.py b/tools/check_cowork_screen.py new file mode 100644 index 0000000..a7331be --- /dev/null +++ b/tools/check_cowork_screen.py @@ -0,0 +1,99 @@ +"""Workspace ▸ Cowork against its wireframe (section 5). + +The drawing heads the screen with the THREAD's title — "Gom số liệu doanh thu", +not the word "Cowork" — with the model beside it, Skills and Cuộc trò chuyện mới +on the right, and a caps TỆP ĐẦU RA (n) panel down the side. +""" +from __future__ import annotations + +import os +import sys + +sys.stdout.reconfigure(encoding="utf-8", errors="replace") + +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +sys.path.insert(0, str(REPO.parent)) +sys.path.insert(0, str(Path(__file__).resolve().parent)) +os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + +from capture_screens import ( # noqa: E402 + _apply_theme, _freeze_schedulers, _isolate_home, _load_fonts) + + +def main() -> int: + sandbox = _isolate_home() + from PySide6.QtWidgets import QApplication + + app = QApplication([]) + _load_fonts() + _freeze_schedulers() + _apply_theme(app) + + from cowork_local.config import CONFIG_DIR + assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}" + + from seed_demo_data import seed + seed() + + from cowork_local.app import MainWindow + from cowork_local.config import AppConfig + from cowork_local.core.history import list_conversations, load_conversation + from cowork_local.i18n import set_language, tr + from cowork_local.state import AppContext + + set_language("vi") + win = MainWindow(AppContext(AppConfig.load()), user_name="local") + win.resize(1920, 1000) + win.show() + app.processEvents() + win._goto(win._ROW_WORKSPACE, win.workspace._cowork_tab_idx) + app.processEvents() + c = win.cowork + fails = [] + + # a new thread has no title yet, so the screen name stands in + print(f"chua mo thread: tieu de={c._title_lbl.text()!r}") + if not c._title_lbl.text().strip(): + fails.append("tieu de trong khi chua mo thread") + + convs = list_conversations() + if not convs: + fails.append("khong co hoi thoai de kiem") + else: + conv = load_conversation(Path(convs[0]["path"])) + c.load_conversation(conv) + app.processEvents() + want = conv.get("title", "") + print(f"sau khi mo thread: tieu de={c._title_lbl.text()!r} (thread={want!r})") + if want and c._title_lbl.text() != want: + fails.append(f"tieu de khong theo thread: {c._title_lbl.text()!r} != {want!r}") + if c._title_lbl.text() == tr("cowork.title") and want: + fails.append("tieu de van la ten man hinh") + + # the files panel is a caps section carrying its own count + head = c.output_section.header.text() + print(f"pane tep dau ra: {head!r}") + body = head.lstrip("▾▸ ").split(" (")[0] + if body != body.upper(): + fails.append(f"tieu de pane chua viet hoa: {head!r}") + if "(" not in head: + fails.append("tieu de pane khong kem so luong") + + # toolbar keeps both actions the drawing shows + for name, btn in (("Skills", c.skills_btn), ("chat moi", c._new_btn)): + if not btn.isVisible(): + 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}") + + print() + for f in fails: + print("FAIL " + f) + print("PASS man Cowork khop ban ve" if not fails else f"{len(fails)} problem(s)") + sys.stdout.flush() + os._exit(1 if fails else 0) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/check_project_screen.py b/tools/check_project_screen.py index 698d054..098dcd0 100644 --- a/tools/check_project_screen.py +++ b/tools/check_project_screen.py @@ -103,7 +103,27 @@ def main() -> int: fails.append(f"thieu nhan {label!r}") print(f"nhan form: {want}") - # 5. the rail's picker must name the projects. It reads project_choices(), + # 5. the drawing heads a populated screen with the title alone; the + # explanation belongs to an empty one. + print(f"hint hien voi {lst.count()} project: {w._hint.isVisible()}") + if lst.count() and w._hint.isVisible(): + fails.append("doan giai thich van hien du da co project") + + # 6. the path is a field in the drawing, not caption text + from PySide6.QtWidgets import QLineEdit + is_field = isinstance(w.folder_lbl, QLineEdit) and w.folder_lbl.isReadOnly() + print(f"o thu muc: {type(w.folder_lbl).__name__} (o nhap chi doc={is_field})") + if not is_field: + fails.append("duong dan thu muc khong phai o nhap chi doc") + + # 7. Lưu project floats at the foot of the panel, not right under the form + save_y = w._save_btn.mapTo(w, QPoint(0, 0)).y() + folder_y = w.folder_lbl.mapTo(w, QPoint(0, 0)).y() + print(f"nut Luu y={save_y}, o thu muc y={folder_y}, cach {save_y - folder_y}px") + if save_y - folder_y < 80: + fails.append("nut Luu khong bi day xuong day panel") + + # 8. the rail's picker must name the projects. It reads project_choices(), # which used to read item.text() — and when rows became widgets the item # text went empty, so every entry showed as a bare folder glyph. Creating # a project is when a user notices, so create one here. diff --git a/ui/chat_panel.py b/ui/chat_panel.py index e3e5a9d..f8e8ef7 100644 --- a/ui/chat_panel.py +++ b/ui/chat_panel.py @@ -84,6 +84,7 @@ class ChatPanel(QWidget): self.session_name = session_name self.session_id = new_session_id() self.title = "" + self._notify_title() # Which project (workspace) this conversation belongs to — every new # thread inherits the currently selected project (Claude-Projects style). self.project_id = "default" @@ -202,7 +203,7 @@ class ChatPanel(QWidget): # Right sidebar: Output files only (see below — Input is tracked but # not shown). self.input_section = CollapsibleSection(tr("widgets.input_files")) - self.output_section = CollapsibleSection(tr("widgets.output_files")) + self.output_section = CollapsibleSection(tr("widgets.output_files").upper()) # Input files are NOT shown in Cowork's UI anymore — but they're still # fully tracked (add/remove/paths()) exactly as before, since that list # is what gets written into the conversation's own "inputs" field on @@ -285,7 +286,7 @@ class ChatPanel(QWidget): self.compress_btn.setText(tr("chatpanel.compress_btn")) self.compress_btn.setToolTip(tr("chatpanel.compress_tooltip")) self.input_section.set_title(tr("widgets.input_files")) - self.output_section.set_title(tr("widgets.output_files")) + self.output_section.set_title(tr("widgets.output_files").upper()) self.plan_section.set_title(tr("widgets.plan_title")) self._io_collapse_btn.setToolTip(tr("chatpanel.collapse_files_tooltip")) self._files_header.setText(tr("chatpanel.files_header")) @@ -1024,6 +1025,7 @@ class ChatPanel(QWidget): if not self.title: base = text or (Path(attachments[0]).name if attachments else "(attachment)") self.title = (base[:60] + "…") if len(base) > 60 else base + self._notify_title() # Reset the Plan panel so each message starts from a clean checklist (the # previous message's plan never lingers/flickers into this one). @@ -1669,6 +1671,12 @@ class ChatPanel(QWidget): self._sync_indicators() 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.""" + hook = getattr(self, "refresh_title", None) + if callable(hook): + hook() + def load_conversation(self, conv: Dict[str, Any]) -> None: """Switch the view to a stored conversation. Allowed while work is running — the current turns keep going in the background.""" @@ -1680,6 +1688,7 @@ class ChatPanel(QWidget): self._detach_live_turns() self.session_id = sid self.title = conv.get("title", "") + self._notify_title() self.project_id = conv.get("project_id", "") or "default" # If this conversation still has a turn running in the background, attach to # its LIVE message list (not a stale disk copy) so the two never race on save. diff --git a/ui/cowork_tab.py b/ui/cowork_tab.py index 7b516d7..44c620f 100644 --- a/ui/cowork_tab.py +++ b/ui/cowork_tab.py @@ -81,8 +81,21 @@ class CoworkTab(ChatPanel): # succeeds (see _cleanup_turn) — never intermediate files or a folder name. on_language_changed(self._retranslate) + def refresh_title(self) -> None: + """Head the screen with the thread you are in, as the drawing does. + + It said "Cowork" on every conversation — the screen's own name, which + the rail already shows. The thread's title is the thing that changes and + the thing that tells you where you are; a thread with no title yet (a + new chat, before its first turn) falls back to the screen name. + """ + lbl = getattr(self, "_title_lbl", None) + if lbl is None: + return # ChatPanel.__init__ sets self.title before we exist + lbl.setText(getattr(self, "title", "") or tr("cowork.title")) + def _retranslate(self) -> None: - self._title_lbl.setText(tr("cowork.title")) + self.refresh_title() self.skills_btn.setText(tr("cowork.skills_btn")) self.skills_btn.setToolTip(tr("cowork.skills_tooltip")) self._new_btn.setText(tr("cowork.new_chat")) diff --git a/ui/workspace_tab.py b/ui/workspace_tab.py index 0b0f234..82dbb9c 100644 --- a/ui/workspace_tab.py +++ b/ui/workspace_tab.py @@ -274,8 +274,11 @@ class WorkspaceTab(QWidget): self._folder_hdr = QLabel() rl.addWidget(self._folder_hdr) folder_row = QHBoxLayout() - self.folder_lbl = QLabel() - self.folder_lbl.setObjectName("hint") + # The drawing shows the path in a field, not as grey caption text. A + # read-only line edit looks like one and, unlike a label, lets the path + # be selected and copied. + self.folder_lbl = QLineEdit() + self.folder_lbl.setReadOnly(True) self._browse_btn = QPushButton() self._browse_btn.setIcon(icon("folder")) self._browse_btn.clicked.connect(self._pick_folder) @@ -287,6 +290,7 @@ class WorkspaceTab(QWidget): folder_row.addWidget(self._open_btn) rl.addLayout(folder_row) + rl.addStretch(1) # the drawing floats Lưu project at the bottom save_row = QHBoxLayout() self._save_btn = QPushButton() self._save_btn.setIcon(icon("save")) @@ -394,7 +398,9 @@ class WorkspaceTab(QWidget): # space taken from Co4E's canvas and Folder's tree on every laptop # screen. Shown where they apply; the text itself is unchanged. self._header.setVisible(on_project) - self._hint.setVisible(on_project) + # ...and the explanation only while there is nothing to explain against: + # the drawing heads a populated screen with the title alone. + self._hint.setVisible(on_project and self.project_list.count() == 0) narrow = getattr(self, "_is_narrow", False) if self._sidebar is not None: self._sidebar.setVisible(on_cowork) @@ -546,6 +552,9 @@ class WorkspaceTab(QWidget): row_to_select = i self.project_list.blockSignals(False) self.project_list.setCurrentRow(row_to_select) + # The drawing heads a populated screen with the title alone; the + # explanation is what an EMPTY one says instead of showing nothing. + self._hint.setVisible(self.project_list.count() == 0) self._load_current() @staticmethod