Feature/fsg gamma team ui fix #3
@@ -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:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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())
|
||||
@@ -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
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user