Feature/fsg gamma team ui fix #3

Merged
gitea-admin merged 41 commits from feature/FSG_GammaTeam_UI_Fix into main 2026-08-20 12:12:59 +00:00
6 changed files with 249 additions and 22 deletions
Showing only changes of commit a0cef768c9 - Show all commits
+10 -2
View File
@@ -1108,8 +1108,16 @@ class MainWindow(QMainWindow):
# Measured from the composer's TOP edge in window coordinates:
# its own height misses the extra row of controls laid out under
# it, which left the dot still overlapping by ~25px.
top = comp.mapTo(self, comp.rect().topLeft()).y()
guard = max(0, self.height() - top + 8)
origin = comp.mapTo(self, comp.rect().topLeft())
# ...but only lift the dot if the composer is actually beneath
# it. The composer stops at the chat column's right edge, well
# short of the dot, so lifting it there raised the dot 156px for
# nothing — on Cowork alone it sat off the corner every other
# screen keeps it in.
dock_left = dock.x() - self.mapToGlobal(self.rect().topLeft()).x()
dock_right = dock_left + dock.width()
if dock_right > origin.x() and dock_left < origin.x() + comp.width():
guard = max(0, self.height() - origin.y() + 8)
dock.set_bottom_guard(guard)
def _on_projects_changed(self) -> None:
+9 -5
View File
@@ -498,13 +498,17 @@ STRINGS: Dict[str, Dict[str, str]] = {
"en": "Ask how to use the app…", "ja": "アプリの使い方を質問…",
"vi": "Hỏi cách sử dụng app…"},
"help_agent.open_tooltip": {
"en": "App Assistant — help using the app",
"ja": "アプリアシスタント — アプリの使い方をサポート",
"vi": "Trợ lý App — hỗ trợ sử dụng app"},
"en": "AI Assistant — help using the app",
"ja": "AI Assistant — アプリの使い方をサポート",
"vi": "AI Assistant — hỗ trợ sử dụng app"},
"help_agent.collapse_tooltip": {
"en": "Minimize", "ja": "最小化", "vi": "Thu nhỏ"},
"help_agent.hide_tooltip": {
"en": "Hide to the edge", "ja": "端に隠す", "vi": "Ẩn vào cạnh phải"},
"help_agent.dot_hint": {
"en": "right-click to hide",
"ja": "右クリックで非表示",
"vi": "chuột phải để ẩn"},
# The name on the launcher pill. Deliberately the same in every language —
# it is a product name, and it only shows on hover, so length is not a
# constraint the way it was on a permanently visible badge.
@@ -513,8 +517,8 @@ STRINGS: Dict[str, Dict[str, str]] = {
"help_agent.more_tooltip": {
"en": "More", "ja": "その他", "vi": "Thêm"},
"help_agent.show_tooltip": {
"en": "Show the App Assistant", "ja": "アプリアシスタントを表示",
"vi": "Hiện App Assistant"},
"en": "Show the AI Assistant", "ja": "AI Assistant を表示",
"vi": "Hiện AI Assistant"},
"help_agent.empty_reply": {
"en": "(no answer)", "ja": "(回答なし)", "vi": "(không có phản hồi)"},
"help_agent.error": {
+102
View File
@@ -0,0 +1,102 @@
"""List every show/hide/enable/disable rule, baseline vs now.
The redesign was allowed to change the flow. It was NOT allowed to change what
is hidden or greyed out — those rules encode real preconditions, and dropping
one turns a guarded action into a broken one.
Reports rules that disappeared, appeared, or changed target between the
pre-redesign commit and HEAD.
"""
from __future__ import annotations
import re
import subprocess
import sys
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
BASE = "291a611"
CALL = re.compile(
r"(?P<target>[\w\.\[\]\(\)_]*?)\.?(?P<verb>setVisible|setHidden|setEnabled|"
r"setDisabled|setTabVisible|setTabEnabled|hide|show)\s*\(")
def files():
out = subprocess.run(["git", "diff", "--name-only", f"{BASE}..HEAD", "--", "*.py"],
capture_output=True, text=True, encoding="utf-8").stdout
return [f for f in out.split() if f.endswith(".py") and not f.startswith("tools/")]
def _arg(s, start):
"""Text inside the call's parentheses — the CONDITION, which matters as
much as the call being there at all."""
depth, out = 0, []
for ch in s[start:]:
if ch == "(":
depth += 1
if depth == 1:
continue
elif ch == ")":
depth -= 1
if depth == 0:
break
if depth >= 1:
out.append(ch)
return "".join(out).strip()
def rules(rev, path):
"""{(target, verb): set(conditions)} for one revision of one file."""
src = subprocess.run(["git", "show", f"{rev}:{path}"],
capture_output=True, text=True, encoding="utf-8",
errors="replace").stdout or ""
found = {}
for n, line in enumerate(src.splitlines(), 1):
s = line.strip()
if s.startswith("#") or s.startswith('"'):
continue
for m in CALL.finditer(s):
target, verb = m.group("target"), m.group("verb")
if not target or (verb in ("hide", "show") and not target):
continue
cond = _arg(s, m.end() - 1) or "-"
found.setdefault((target, verb), {}).setdefault(cond, n)
return found
def main() -> int:
gone, added, changed = [], [], []
for path in files():
old, new = rules(BASE, path), rules("HEAD", path)
for key in sorted(set(old) - set(new)):
gone.append((path, key, old[key]))
for key in sorted(set(new) - set(old)):
added.append((path, key, new[key]))
for key in sorted(set(new) & set(old)):
if set(old[key]) != set(new[key]):
changed.append((path, key, old[key], new[key]))
print(f"=== A. LUAT BI BO ({len(gone)}) ===")
for path, (target, verb), conds in gone:
for cond, line in conds.items():
print(f" {path}:{line:<5} {target}.{verb}({cond})")
print()
print(f"=== B. DIEU KIEN DOI ({len(changed)}) ===")
for path, (target, verb), oldc, newc in changed:
print(f" {path} {target}.{verb}()")
for c in sorted(set(oldc) - set(newc)):
print(f" cu : ({c})")
for c in sorted(set(newc) - set(oldc)):
print(f" moi : ({c}) dong {newc[c]}")
print()
print(f"=== C. LUAT MOI THEM ({len(added)}) ===")
for path, (target, verb), conds in added:
for cond, line in conds.items():
print(f" {path}:{line:<5} {target}.{verb}({cond})")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+94
View File
@@ -0,0 +1,94 @@
"""The assistant dot sits in the bottom-right corner — unless the composer is
genuinely underneath it.
It used to lift on Cowork whenever a composer existed, measured only on the
vertical axis. On a wide window the composer stops at the chat column's right
edge, far short of the dot, so the dot rose 156px for nothing and Cowork was
the one screen where it was not in the corner.
"""
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.i18n import set_language
from cowork_local.state import AppContext
set_language("vi")
win = MainWindow(AppContext(AppConfig.load()), user_name="local")
win.show()
dock = win.help_agent
fails = []
def gap_and_overlap(width, height):
win.resize(width, height)
app.processEvents()
win._goto(win._ROW_WORKSPACE, win.workspace._cowork_tab_idx)
app.processEvents()
comp = win.cowork.composer
origin = comp.mapTo(win, comp.rect().topLeft())
dleft = dock.x() - win.mapToGlobal(win.rect().topLeft()).x()
overlaps = (dleft + dock.width() > origin.x()
and dleft < origin.x() + comp.width())
gap = win.height() - (dock.y() + dock.height())
return gap, overlaps, origin.x() + comp.width(), dleft
# baseline: every other screen
win.resize(1936, 1048)
app.processEvents()
win._goto(win._ROW_WORKSPACE, win.workspace._project_tab_idx)
app.processEvents()
corner = win.height() - (dock.y() + dock.height())
print(f"man thuong : cach day {corner}px")
for w, h in ((1936, 1048), (1200, 800), (900, 700)):
gap, over, comp_right, dleft = gap_and_overlap(w, h)
print(f"Cowork {w}x{h:<5}: cach day {gap:>4}px | composer het o x={comp_right} "
f"| cham o x={dleft} | chong nhau={over}")
if over and gap <= corner:
fails.append(f"{w}x{h}: composer nam duoi cham ma cham khong duoc nang")
if not over and gap != corner:
fails.append(f"{w}x{h}: khong chong nhau ma cham van lech "
f"({gap}px thay vi {corner}px)")
print()
for f in fails:
print("FAIL " + f)
print("PASS cham o goc, chi nang khi that su bi che" 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())
+20 -2
View File
@@ -223,7 +223,8 @@ class HelpAgentWidget(QWidget):
self.launcher.setObjectName("helpLauncher")
self.launcher.setIcon(icon("sparkle", size=_DOT_ICON, color=_SPARK_GOLD))
self.launcher.setCursor(Qt.PointingHandCursor)
self.launcher.setToolTip(tr("help_agent.open_tooltip"))
self.launcher.setToolTip(
f'{tr("help_agent.open_tooltip")} · {tr("help_agent.dot_hint")}')
self.launcher.clicked.connect(self._expand)
def _build_panel(self) -> None:
@@ -267,6 +268,12 @@ class HelpAgentWidget(QWidget):
self.act_collapse.triggered.connect(self._collapse)
self.act_hide = menu.addAction(tr("help_agent.hide_tooltip"))
self.act_hide.triggered.connect(self._hide_to_edge)
# The old build put a chevron button beside the dot that hid the
# assistant in one click. The drawing has no such button — the dot is
# 26px, no text, no chevron — so the same reach comes back as a right-
# click on the dot rather than as pixels next to it.
self.launcher.setContextMenuPolicy(Qt.CustomContextMenu)
self.launcher.customContextMenuRequested.connect(self._dot_menu)
self.more_btn.setMenu(menu)
hb.addWidget(self.more_btn)
v.addWidget(header)
@@ -309,6 +316,16 @@ class HelpAgentWidget(QWidget):
self._state = _LAUNCHER_ST
self._apply_state()
def _dot_menu(self, pos) -> None:
"""Right-click on the dot: hide to the edge, the one action that means
anything while the panel is shut ("collapse to dot" already happened)."""
from PySide6.QtWidgets import QMenu
menu = QMenu(self.launcher)
act = menu.addAction(tr("help_agent.hide_tooltip"))
act.triggered.connect(self._hide_to_edge)
menu.exec(self.launcher.mapToGlobal(pos))
def _hide_to_edge(self) -> None:
self._state = _HIDDEN
self._apply_state()
@@ -460,7 +477,8 @@ class HelpAgentWidget(QWidget):
self._render()
self.title.setText(tr("help_agent.title"))
self.input.setPlaceholderText(tr("help_agent.placeholder"))
self.launcher.setToolTip(tr("help_agent.open_tooltip"))
self.launcher.setToolTip(
f'{tr("help_agent.open_tooltip")} · {tr("help_agent.dot_hint")}')
if self.launcher.open:
self.launcher.setText(f" {tr('help_agent.badge')}")
self._layout_launcher()
+14 -13
View File
@@ -459,13 +459,16 @@ class MonitoringTab(QWidget):
self.ov_usage_cache = StatCard()
self.ov_usage_cost = StatCard()
self.ov_usage_calls = StatCard()
for i, card in enumerate((self.ov_usage_total, self.ov_usage_cost,
self.ov_usage_calls)):
# The wireframe's usage block is five figures: cost (with the turn
# count on its label), then Tổng token / Input / Output / Cache. Folding
# the last three into a sub-line took their PER-PART COST off screen —
# the total tile has room for the token counts but not for three more
# prices — and the drawing asks for the tiles anyway.
for i, card in enumerate((self.ov_usage_cost, self.ov_usage_total,
self.ov_usage_in, self.ov_usage_out,
self.ov_usage_cache)):
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)
self.ov_usage_calls.setVisible(False) # rides on the cost tile's label
# 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()
@@ -916,11 +919,7 @@ 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"]),
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_total.set(tr("dashboard.card_total"), fmt_tokens(s["total"]))
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))
@@ -928,8 +927,10 @@ class MonitoringTab(QWidget):
ut.format_cost(costs["out"], pricing))
self.ov_usage_cache.set(tr("dashboard.card_cache"), fmt_tokens(s["cache"]),
ut.format_cost(costs["cache"], pricing))
self.ov_usage_cost.set(tr("dashboard.card_cost"),
ut.format_cost(sum(costs.values()), pricing, digits=2))
# "Tổng chi phí · 57 lượt", exactly as the wireframe labels it.
self.ov_usage_cost.set(
f'{tr("dashboard.card_cost")} · {s["turns"]} {tr("monitoring.overview_calls")}',
ut.format_cost(sum(costs.values()), pricing, digits=2))
self._refresh_budget()
def _sync_sbx_more_label(self, *_a) -> None: