feat(ui): flat nav rail, compact assistant, responsive layouts

Implements the redesign from docs/ui-audit.html. Rearrangement only — no
feature was removed; every control that moved kept its handler, and the
gates that used to hide things now grey them out instead.

Navigation
  * The rail is one flat list: the five Workspace sub-views sit at the top
    level instead of behind an accordion, with Dashboard/Monitoring pinned
    at the foot and Settings below them.
  * Cowork and GraphRAG stay listed and greyed while no project is
    selected, rather than vanishing and resizing the menu under the user.
  * Monitoring keeps its eight sub-views in its own tab strip (unhidden)
    instead of doubling the rail's length.
  * _goto now moves the highlight itself, fixing a long-standing bug where
    programmatic navigation left the rail pointing at the previous screen.
  * Rail header gained the project picker and "New chat"; RECENTS lists the
    active project's threads. Both are second views of existing state — the
    Cowork toolbar button and the full History panel are untouched.
  * Provider / language / theme moved from the top bar to an account row at
    the foot of the rail (same widgets, same signals).

Screens
  * Co4E: the flow tab strip is gone (per the design); Flow Status became a
    toolbar toggle with its own way back, and the three icon-only tabs became
    four labelled, foldable sections in one column. One flow open at a time
    is the one capability this costs; background runs are unaffected.
  * Dashboard: header split into two rows; cost promoted to a hero card.
  * Monitoring Overview: one scrolling column of titled sections; the model
    price table got its own full-width section instead of sharing a row with
    the CPU meters.
  * Settings and Task editor gained a section index down the left.
  * Help dock: 84x64 launcher + chevron became one 26px dot that expands to
    a labelled pill on hover; "hide to the edge" moved into the panel's menu.

Layout
  * The window's minimum width dropped from 1453px to 768px. The main cause
    was a QTabWidget taking its minimum from the widest page even when that
    page is hidden, so Co4E was forcing Project and Cowork wide.
  * Secondary panes fold themselves on a narrow window and restore when it
    grows, never overriding a fold the user made.
  * The long dialogs no longer scroll sideways at any font size.

Verification: tools/check_*.py build a real MainWindow offscreen against a
copy of ~/.cowork_local with the schedulers no-oped. check_design_parity.py
reads its checklist straight from the audit page's own proposals.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
NamPDT
2026-08-17 11:40:13 +09:00
co-authored by Claude Opus 5
parent 291a611737
commit 0fa61b6a95
27 changed files with 4346 additions and 384 deletions
+131
View File
@@ -0,0 +1,131 @@
"""Check the redesigned help dock on the real widget, offscreen.
The claim being made is a size claim ("84×64 → 26×26"), so this measures the
widget instead of trusting the constants, and confirms that nothing the old
three-button layout could do has gone missing — hiding to the edge just moved
into the panel's ⋯ menu.
Run: python tools/check_help_dock.py
"""
from __future__ import annotations
import os
import sys
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 _isolate_home, _load_fonts # noqa: E402
def main() -> int:
sandbox = _isolate_home()
from PySide6.QtWidgets import QApplication, QWidget
app = QApplication([])
_load_fonts()
from cowork_local.config import AppConfig, CONFIG_DIR
assert str(sandbox) in str(CONFIG_DIR), f"isolation failed: {CONFIG_DIR}"
from cowork_local.i18n import set_language, tr
from cowork_local.state import AppContext
from cowork_local.ui.help_agent_widget import HelpAgentWidget
set_language("vi")
host = QWidget()
host.resize(1200, 800)
dock = HelpAgentWidget(AppContext(AppConfig.load()), host, user_name="local")
app.processEvents()
fails: list[str] = []
OLD_W, OLD_H = 84, 64 # 64px badge + 2px gap + 18px chevron
closed = dock.size()
print(f"dong : {closed.width()}x{closed.height()}px "
f"(cu {OLD_W}x{OLD_H})")
area_new, area_old = closed.width() * closed.height(), OLD_W * OLD_H
print(f"dien tich : {area_new} vs {area_old}px2 "
f"({100 - round(area_new / area_old * 100)}% nho hon)")
if closed.width() > 30 or closed.height() > 30:
fails.append(f"nut dong van {closed.width()}x{closed.height()}, cho <=30")
# 26px clears the ~24px comfortable-tap floor the old 18px chevron missed.
if min(closed.width(), closed.height()) < 24:
fails.append("vung bam nho hon 24px")
# Hover: the name appears, and only then.
print(f"chu luc dong : {dock.launcher.text()!r} (phai rong)")
if dock.launcher.text().strip():
fails.append("nut dong ma van hien chu")
dock.launcher._set_open(True)
app.processEvents()
hovered = dock.size()
print(f"re chuot : {hovered.width()}x{hovered.height()}px · "
f"chu = {dock.launcher.text().strip()!r}")
if tr("help_agent.badge") not in dock.launcher.text():
fails.append("re chuot khong hien 'AI Assistant'")
if hovered.width() <= closed.width():
fails.append("re chuot ma nut khong no ra")
dock.launcher._set_open(False)
app.processEvents()
if dock.size().width() != closed.width():
fails.append("roi chuot ma nut khong thu lai")
# Every state still reachable, and the corner anchor still holds.
for state, call in (("panel", dock._expand), ("launcher", dock._collapse),
("hidden", dock._hide_to_edge), ("launcher", dock._show_launcher)):
call()
app.processEvents()
got = dock._state
inside = (dock.x() + dock.width() <= host.width()
and dock.y() + dock.height() <= host.height())
print(f"trang thai {state:9}: {got:9} {dock.width():3}x{dock.height():3} "
f"goc phai duoi = {inside}")
if got != state:
fails.append(f"khong vao duoc trang thai {state}")
if not inside:
fails.append(f"trang thai {state} tran ra ngoai cua so")
# The edge tab was 16px — below anything comfortable to hit.
dock._hide_to_edge()
app.processEvents()
print(f"tab mep : {dock.width()}px (cu 16px)")
if dock.width() < 24:
fails.append(f"tab mep {dock.width()}px, van duoi 24px")
dock._show_launcher()
# Nothing removed: "hide to the edge" is in the ⋯ menu now.
items = [a.text() for a in dock.more_btn.menu().actions()]
print(f"menu ⋯ : {items}")
for key in ("help_agent.collapse_tooltip", "help_agent.hide_tooltip"):
if tr(key) not in items:
fails.append(f"menu thieu muc {tr(key)}")
print(f"nut thu nho : {dock.min_btn.toolTip()!r}")
print(f"nut gui / o nhap: {dock.send_btn is not None} / {dock.input is not None}")
# All three languages must have the new strings.
for lang in ("vi", "en", "ja"):
set_language(lang)
dock.retranslate()
vals = [dock.launcher.toolTip(), tr("help_agent.badge"),
tr("help_agent.more_tooltip")]
print(f" {lang}: badge={vals[1]!r} more={vals[2]!r}")
if any(not v or v.startswith("help_agent.") for v in vals):
fails.append(f"thieu ban dich cho {lang}")
set_language("vi")
print()
if fails:
print("*** LOI ***")
for f in fails:
print(" " + f)
return 1
print("KET QUA: nut tro ly gon lai, khong mat chuc nang nao")
return 0
if __name__ == "__main__":
raise SystemExit(main())