chore: sync local working copy as of 2026-08-15
The Gitea repo was initialised from an earlier snapshot, so main and the machine this runs on had drifted apart in 153 files before any UI work started. This commit brings the branch up to the local tree as it stood on 2026-08-15 21:31 (from cowork_local.7z), so the redesign that follows shows up as its own reviewable diff instead of being mixed in with the pre-existing divergence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+2
-2
@@ -28,7 +28,7 @@ from ..core import usage_tracker as ut
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..state import AppContext
|
||||
from .icons import icon
|
||||
from .icons import DOT_AMBER, icon
|
||||
from .widgets import fmt_tokens
|
||||
|
||||
_PERIODS = ("day", "week", "month", "year")
|
||||
@@ -367,7 +367,7 @@ class AccountsTab(QWidget):
|
||||
label = f"{acc.display_name or acc.username} ({acc.username}) — {tr(f'accounts.role.{acc.role}')}"
|
||||
item = QTreeWidgetItem([label])
|
||||
if is_subadmin: # subadmin badge → star icon instead of a ★ glyph
|
||||
item.setIcon(0, icon("star", color="#f59e0b"))
|
||||
item.setIcon(0, icon("star", color=DOT_AMBER))
|
||||
item.setData(0, Qt.UserRole, ("account", acc.username))
|
||||
if acc.email:
|
||||
item.setToolTip(0, acc.email)
|
||||
|
||||
+13
-11
@@ -20,6 +20,7 @@ from ..core.calendar_grid import (
|
||||
GRANULARITIES, group_tasks_by_date, month_grid, month_task_counts, shift_period, week_days,
|
||||
)
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..theme import current_palette
|
||||
from .icons import icon
|
||||
|
||||
_WEEKDAY_KEYS = ("mon", "tue", "wed", "thu", "fri", "sat", "sun")
|
||||
@@ -56,20 +57,21 @@ class _DayCell(QFrame):
|
||||
today: bool = False, weekend: bool = False) -> None:
|
||||
self._date_str = d.isoformat()
|
||||
self.date_lbl.setText(str(d.day))
|
||||
num_color = "#0096C7" if today else ("#888" if dim else "")
|
||||
self.date_lbl.setStyleSheet(f"font-weight:700; color:{num_color};")
|
||||
# Today = accent border + stronger tint; weekend (Sat/Sun) = a subtle
|
||||
# darker-blue tint than the base cell. rgba overlays read correctly on
|
||||
# both light and dark themes.
|
||||
base_border = "1px solid rgba(128,128,128,0.35)"
|
||||
p = current_palette()
|
||||
num_color = p.accent if today else (p.text_faint if dim else p.text)
|
||||
self.date_lbl.setStyleSheet(f"font-weight:600; color:{num_color};")
|
||||
# Today is the only cell that gets a filled surface + accent border;
|
||||
# weekends are set apart by a recessed surface alone, so the eye lands
|
||||
# on "today" first and on the weekend block only when scanning.
|
||||
r = p.radius
|
||||
if today:
|
||||
css = ("#dayCell { background: rgba(0,150,199,0.22); "
|
||||
"border: 2px solid #0096C7; border-radius: 6px; }")
|
||||
css = (f"#dayCell {{ background: {p.accent_soft}; "
|
||||
f"border: 1px solid {p.accent}; border-radius: {r}px; }}")
|
||||
elif weekend:
|
||||
css = ("#dayCell { background: rgba(0,120,182,0.13); "
|
||||
f"border: {base_border}; border-radius: 6px; }}")
|
||||
css = (f"#dayCell {{ background: {p.surface}; "
|
||||
f"border: 1px solid {p.border}; border-radius: {r}px; }}")
|
||||
else:
|
||||
css = f"#dayCell {{ border: {base_border}; border-radius: 6px; }}"
|
||||
css = f"#dayCell {{ border: 1px solid {p.border}; border-radius: {r}px; }}"
|
||||
self.setStyleSheet(css)
|
||||
self.list.clear()
|
||||
for t in tasks:
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ from PySide6.QtWidgets import (
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..state import AppContext
|
||||
from ..theme import current_palette
|
||||
from .chat_view import ChatView, ThinkingIndicator
|
||||
from .composer import Composer
|
||||
from .icons import collapse_right_icon, icon as app_icon
|
||||
@@ -140,7 +141,7 @@ class ChatPanel(QWidget):
|
||||
# updated after each turn; cost uses the Monitoring model-price table.
|
||||
self._usage_total_lbl = QLabel("")
|
||||
self._usage_total_lbl.setObjectName("hint")
|
||||
self._usage_total_lbl.setStyleSheet("color: rgba(140,146,152,0.9);")
|
||||
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 /
|
||||
|
||||
+50
-53
@@ -12,7 +12,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..theme import ACCENT, resolve_theme
|
||||
from ..theme import palette, resolve_theme
|
||||
from ..config import CONFIG_DIR
|
||||
from .osutil import is_image, open_folder, open_path
|
||||
|
||||
@@ -28,11 +28,18 @@ def _app_theme() -> str:
|
||||
return "dark"
|
||||
|
||||
|
||||
# Timeline dot color per role (reads on both themes — small, saturated).
|
||||
_DOT = {
|
||||
"user": "#48CAE4", "assistant": "#48D9A0", "tool": "#9B8FF7",
|
||||
"error": "#E5484D", "success": "#48D9A0",
|
||||
}
|
||||
def _p():
|
||||
"""Design tokens for the theme in effect right now."""
|
||||
return palette(_app_theme())
|
||||
|
||||
|
||||
def _dot_color(role: str) -> str:
|
||||
"""Timeline dot colour for a message role."""
|
||||
p = _p()
|
||||
return {
|
||||
"user": p.role_user, "assistant": p.role_assistant, "tool": p.role_tool,
|
||||
"error": p.role_error, "success": p.role_result,
|
||||
}.get(role, p.text_faint)
|
||||
|
||||
|
||||
class _TimelineGutter(QWidget):
|
||||
@@ -52,17 +59,17 @@ class _TimelineGutter(QWidget):
|
||||
def paintEvent(self, _e): # noqa: N802
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
dark = _app_theme() == "dark"
|
||||
tok = _p()
|
||||
x = 11.0
|
||||
cy = 15.0
|
||||
# connector line (faint) running the full height → continuous rail
|
||||
p.setPen(QPen(QColor("#243a56" if dark else "#CBDDEC"), 2))
|
||||
p.setPen(QPen(QColor(tok.border), 2))
|
||||
p.drawLine(int(x), 0, int(x), self.height())
|
||||
# a background ring lifts the dot off the line
|
||||
p.setPen(Qt.NoPen)
|
||||
p.setBrush(QColor("#0A1628" if dark else "#E8F4FD"))
|
||||
p.setBrush(QColor(tok.bg))
|
||||
p.drawEllipse(QPointF(x, cy), 7.5, 7.5)
|
||||
p.setBrush(QColor(_DOT.get(self._role, "#8FB2D4")))
|
||||
p.setBrush(QColor(_dot_color(self._role)))
|
||||
p.drawEllipse(QPointF(x, cy), 4.5, 4.5)
|
||||
|
||||
|
||||
@@ -72,18 +79,20 @@ def _diff_legend(diff_text: str) -> str:
|
||||
so the before/after distinction is explicit, not just implied by color."""
|
||||
has_add = any(ln.startswith("+") and not ln.startswith("+++") for ln in diff_text.splitlines())
|
||||
has_del = any(ln.startswith("-") and not ln.startswith("---") for ln in diff_text.splitlines())
|
||||
before = (f'<span style="background:#3a1620; color:#ff9aa8; padding:1px 8px; '
|
||||
f'border-radius:4px; font-weight:600;">{html.escape(tr("chat.diff_before"))}</span>')
|
||||
after = (f'<span style="background:#0d3321; color:#7ee2a8; padding:1px 8px; '
|
||||
f'border-radius:4px; font-weight:600;">{html.escape(tr("chat.diff_after"))}</span>')
|
||||
p = _p()
|
||||
|
||||
def pill(bg: str, fg: str, key: str) -> str:
|
||||
return (f'<span style="background:{bg}; color:{fg}; padding:1px 8px; '
|
||||
f'border-radius:4px; font-weight:600;">{html.escape(tr(key))}</span>')
|
||||
|
||||
if has_add and has_del:
|
||||
badge = f'{before}<span style="color:#8b8d98;"> → </span>{after}'
|
||||
badge = (pill(p.diff_del_bg, p.diff_del_fg, "chat.diff_before")
|
||||
+ f'<span style="color:{p.text_muted};"> → </span>'
|
||||
+ pill(p.diff_add_bg, p.diff_add_fg, "chat.diff_after"))
|
||||
elif has_add:
|
||||
badge = (f'<span style="background:#0d3321; color:#7ee2a8; padding:1px 8px; '
|
||||
f'border-radius:4px; font-weight:600;">{html.escape(tr("chat.diff_added"))}</span>')
|
||||
badge = pill(p.diff_add_bg, p.diff_add_fg, "chat.diff_added")
|
||||
elif has_del:
|
||||
badge = (f'<span style="background:#3a1620; color:#ff9aa8; padding:1px 8px; '
|
||||
f'border-radius:4px; font-weight:600;">{html.escape(tr("chat.diff_removed"))}</span>')
|
||||
badge = pill(p.diff_del_bg, p.diff_del_fg, "chat.diff_removed")
|
||||
else:
|
||||
return ""
|
||||
return f'<div style="margin-bottom:6px;">{badge}</div>'
|
||||
@@ -97,21 +106,22 @@ def diff_to_html(diff_text: str) -> str:
|
||||
empty 'before') naturally renders as all-green, which is exactly what
|
||||
``difflib.unified_diff`` already produces for it."""
|
||||
legend = _diff_legend(diff_text)
|
||||
p = _p()
|
||||
rows = []
|
||||
for ln in diff_text.splitlines():
|
||||
esc = html.escape(ln) if ln else " "
|
||||
if ln.startswith(("+++", "---")):
|
||||
rows.append(f'<div style="color:#8b8d98;">{esc}</div>')
|
||||
rows.append(f'<div style="color:{p.text_muted};">{esc}</div>')
|
||||
elif ln.startswith("@@"):
|
||||
rows.append(f'<div style="color:#7c8aff;">{esc}</div>')
|
||||
rows.append(f'<div style="color:{p.accent};">{esc}</div>')
|
||||
elif ln.startswith("+"):
|
||||
rows.append(f'<div style="background:#0d3321; color:#7ee2a8;">{esc}</div>')
|
||||
rows.append(f'<div style="background:{p.diff_add_bg}; color:{p.diff_add_fg};">{esc}</div>')
|
||||
elif ln.startswith("-"):
|
||||
rows.append(f'<div style="background:#3a1620; color:#ff9aa8;">{esc}</div>')
|
||||
rows.append(f'<div style="background:{p.diff_del_bg}; color:{p.diff_del_fg};">{esc}</div>')
|
||||
else:
|
||||
rows.append(f"<div>{esc}</div>")
|
||||
body = "".join(rows) or "(no textual change)"
|
||||
return (f'{legend}<div style="font-family:Consolas,\'Courier New\',monospace; font-size:12.5px; '
|
||||
return (f'{legend}<div style="font-family:{p.font_mono}; font-size:12.5px; '
|
||||
f'white-space:pre-wrap;">{body}</div>')
|
||||
|
||||
|
||||
@@ -219,12 +229,12 @@ class MessageBubble(QFrame):
|
||||
self._head.setCursor(Qt.PointingHandCursor)
|
||||
self._head.setStyleSheet(
|
||||
"QPushButton { text-align:left; border:none; background:transparent;"
|
||||
" font-weight:600; color:#8b8d98; padding:0; }")
|
||||
f" font-weight:600; color:{_p().text_muted}; padding:0; }}")
|
||||
self._head.clicked.connect(self._toggle_body)
|
||||
lay.addWidget(self._head)
|
||||
else:
|
||||
head = QLabel(title)
|
||||
head.setStyleSheet("font-weight:600; color:#8b8d98;")
|
||||
head.setStyleSheet(f"font-weight:600; color:{_p().text_muted};")
|
||||
lay.addWidget(head)
|
||||
|
||||
self.body = QTextBrowser()
|
||||
@@ -265,36 +275,23 @@ class MessageBubble(QFrame):
|
||||
|
||||
def _apply_theme_styles(self, role: str) -> None:
|
||||
"""Apply text color to the body QTextBrowser based on current theme + role."""
|
||||
theme = self._current_theme()
|
||||
if theme == "light":
|
||||
if role == "success":
|
||||
text_color = "#1B7A3D"
|
||||
elif role == "error":
|
||||
text_color = "#C0392B"
|
||||
elif role in ("tool",):
|
||||
text_color = "#5C6B7A" # muted (secondary) like Claude's steps
|
||||
else:
|
||||
text_color = "#1A2332"
|
||||
else:
|
||||
if role == "success":
|
||||
text_color = "#7ee2a8"
|
||||
elif role == "error":
|
||||
text_color = "#ff9aa8"
|
||||
elif role in ("tool",):
|
||||
text_color = "#9aa6b8"
|
||||
else:
|
||||
text_color = "#eceef2"
|
||||
p = _p()
|
||||
text_color = {
|
||||
"success": p.success,
|
||||
"error": p.danger,
|
||||
"tool": p.text_muted, # secondary, like Claude's steps
|
||||
}.get(role, p.text)
|
||||
self.body.setStyleSheet(f"background: transparent; border: none; color: {text_color};")
|
||||
|
||||
def _apply_style(self, role: str) -> None:
|
||||
"""Flat timeline row — no bubble box; the left dot/rail conveys role and
|
||||
structure (Claude-Code style). The user's own message gets a faint tint
|
||||
so questions are easy to pick out when scanning."""
|
||||
theme = self._current_theme()
|
||||
p = _p()
|
||||
if role == "user":
|
||||
tint = "rgba(72,202,228,0.10)" if theme == "dark" else "rgba(72,202,228,0.14)"
|
||||
self.setStyleSheet(
|
||||
f"QFrame {{ background: {tint}; border: none; border-radius: 10px; }}")
|
||||
f"QFrame {{ background: {p.surface}; border: none; "
|
||||
f"border-radius: {p.radius}px; }}")
|
||||
else:
|
||||
self.setStyleSheet("QFrame { background: transparent; border: none; }")
|
||||
|
||||
@@ -353,20 +350,20 @@ class MessageBubble(QFrame):
|
||||
existing.setText(text)
|
||||
return
|
||||
lbl = QLabel(text)
|
||||
lbl.setObjectName("hint")
|
||||
lbl.setStyleSheet("color: rgba(140,146,152,0.9); font-size: 11px;")
|
||||
lbl.setObjectName("faint")
|
||||
lbl.setStyleSheet(f"color: {_p().text_faint}; font-size: 11px;")
|
||||
self._usage_lbl = lbl
|
||||
self._content_layout.addWidget(lbl)
|
||||
|
||||
def add_delete_link(self, callback) -> None:
|
||||
link = QLabel(f'<a href="#del" style="color:#ef6368;">{tr("chat.delete_link")}</a>')
|
||||
link = QLabel(f'<a href="#del" style="color:{_p().danger};">{tr("chat.delete_link")}</a>')
|
||||
link.setToolTip(tr("chat.delete_tooltip"))
|
||||
link.linkActivated.connect(lambda *_: callback())
|
||||
self._content_layout.addWidget(link)
|
||||
|
||||
def add_folder_link(self, folder: str, label: str | None = None) -> None:
|
||||
label = label or tr("chat.open_workspace")
|
||||
link = QLabel(f'<a href="#open" style="color:{ACCENT};">{label}</a>')
|
||||
link = QLabel(f'<a href="#open" style="color:{_p().accent};">{label}</a>')
|
||||
link.setToolTip(str(folder))
|
||||
link.linkActivated.connect(lambda *_: open_folder(folder))
|
||||
self._content_layout.addWidget(link)
|
||||
@@ -385,7 +382,7 @@ class MessageBubble(QFrame):
|
||||
thumb.setCursor(Qt.PointingHandCursor)
|
||||
self._content_layout.addWidget(thumb)
|
||||
continue
|
||||
file_link = QLabel(f'<a href="#open" style="color:{ACCENT};">{name}</a>')
|
||||
file_link = QLabel(f'<a href="#open" style="color:{_p().accent};">{name}</a>')
|
||||
file_link.setToolTip(path)
|
||||
file_link.linkActivated.connect(lambda *_a, fp=path: open_path(fp))
|
||||
self._content_layout.addWidget(file_link)
|
||||
|
||||
+33
-19
@@ -27,13 +27,20 @@ from ..core.co4e import (
|
||||
STEP_DONE, STEP_ERROR, STEP_PLANNED, STEP_RUNNING, Edge, Node, Step,
|
||||
compute_waves, new_edge_id, new_node_id,
|
||||
)
|
||||
from ..theme import current_palette
|
||||
|
||||
|
||||
def _status_color(status: str) -> str:
|
||||
"""Accent colour for a step's run status. Resolved per paint so the canvas
|
||||
follows a live theme switch."""
|
||||
p = current_palette()
|
||||
return {
|
||||
"idle": p.text_muted, STEP_RUNNING: p.accent, STEP_DONE: p.success,
|
||||
STEP_ERROR: p.danger, STEP_PLANNED: p.purple, "pending": p.text_faint,
|
||||
}.get(status, p.text_muted)
|
||||
|
||||
CO4E_MIME = "application/x-co4e-step"
|
||||
|
||||
_STATUS_COLOR = {
|
||||
"idle": "#5C8DB8", STEP_RUNNING: "#48CAE4", STEP_DONE: "#48D9A0",
|
||||
STEP_ERROR: "#E5484D", STEP_PLANNED: "#9B8FF7", "pending": "#7A8DA8",
|
||||
}
|
||||
_NODE_W, _NODE_H = 210, 96
|
||||
_PORT_R = 6 # output port radius (the drag-to-connect handle)
|
||||
_PORT_HIT = 15 # click tolerance around a port
|
||||
@@ -63,24 +70,29 @@ class _NodeItem(QGraphicsObject):
|
||||
return QRectF(1, 1, _NODE_W - 2, _NODE_H - 2)
|
||||
|
||||
def paint(self, p, _opt, _widget=None):
|
||||
tok = current_palette()
|
||||
step = self.node.data
|
||||
accent = QColor(_STATUS_COLOR.get(self.status, "#5C8DB8"))
|
||||
body = QColor("#0D1F35")
|
||||
border = QColor("#48CAE4") if self.isSelected() else QColor("#1A2D4A")
|
||||
accent = QColor(_status_color(self.status))
|
||||
body = QColor(tok.surface_raised)
|
||||
border = QColor(tok.accent) if self.isSelected() else QColor(tok.border)
|
||||
p.setRenderHint(p.RenderHint.Antialiasing)
|
||||
rect = self._card_rect()
|
||||
path = QPainterPath()
|
||||
path.addRoundedRect(rect, 10, 10)
|
||||
radius = float(tok.radius_lg)
|
||||
path.addRoundedRect(rect, radius, radius)
|
||||
p.fillPath(path, QBrush(body))
|
||||
p.setPen(QPen(border, 2 if self.isSelected() else 1))
|
||||
p.drawPath(path)
|
||||
# header stripe
|
||||
# header stripe — a tint of the status colour, not the status colour
|
||||
# itself, so the card's own text stays the brightest thing on it.
|
||||
hdr = QRectF(rect.left(), rect.top(), rect.width(), 26)
|
||||
hpath = QPainterPath()
|
||||
hpath.addRoundedRect(hdr, 10, 10)
|
||||
p.fillPath(hpath, QBrush(accent.darker(160)))
|
||||
hpath.addRoundedRect(hdr, radius, radius)
|
||||
stripe = QColor(accent)
|
||||
stripe.setAlpha(48)
|
||||
p.fillPath(hpath, QBrush(stripe))
|
||||
# label
|
||||
p.setPen(QColor("#E0F0FF"))
|
||||
p.setPen(QColor(tok.text))
|
||||
f = p.font(); f.setBold(True); f.setPointSize(9); p.setFont(f)
|
||||
p.drawText(QRectF(10, 4, _NODE_W - 20, 20), Qt.AlignVCenter | Qt.AlignLeft,
|
||||
_elide(step.label, 26))
|
||||
@@ -89,7 +101,7 @@ class _NodeItem(QGraphicsObject):
|
||||
p.setPen(accent)
|
||||
p.drawText(QRectF(10, 30, _NODE_W - 20, 16), Qt.AlignLeft, step.role)
|
||||
# body: instructions preview OR sub-agent chips
|
||||
p.setPen(QColor("#8FB2D4"))
|
||||
p.setPen(QColor(tok.text_muted))
|
||||
if step.is_parallel:
|
||||
preview = "⇉ " + ", ".join(s.agent for s in step.sub_agents) if step.sub_agents else "⇉ (no sub-agents)"
|
||||
else:
|
||||
@@ -97,7 +109,7 @@ class _NodeItem(QGraphicsObject):
|
||||
p.drawText(QRectF(10, 46, _NODE_W - 20, 30), Qt.TextWordWrap | Qt.AlignTop,
|
||||
_elide(preview, 66))
|
||||
# footer: model + skills + status dot
|
||||
p.setPen(QColor("#5C8DB8"))
|
||||
p.setPen(QColor(tok.text_faint))
|
||||
foot = []
|
||||
if step.model:
|
||||
foot.append(step.model)
|
||||
@@ -109,7 +121,7 @@ class _NodeItem(QGraphicsObject):
|
||||
# ---- ports ---------------------------------------------------------
|
||||
# input port (top-center): hollow. output port (bottom-center): filled —
|
||||
# the drag handle you pull to wire an edge to another step.
|
||||
port_col = QColor("#48CAE4")
|
||||
port_col = QColor(tok.accent)
|
||||
# input port (left-center): hollow. output port (right-center): filled —
|
||||
# the drag handle you pull to wire an edge to the next step (left→right).
|
||||
p.setBrush(QBrush(body)); p.setPen(QPen(port_col, 1.4))
|
||||
@@ -298,12 +310,13 @@ class _EdgeItem(QGraphicsPathItem):
|
||||
self._apply_pen()
|
||||
|
||||
def _apply_pen(self):
|
||||
tok = current_palette()
|
||||
if self.isSelected():
|
||||
color, w = QColor("#48CAE4"), 3
|
||||
color, w = QColor(tok.accent), 3
|
||||
elif self._hover:
|
||||
color, w = QColor("#6FA8C8"), 3
|
||||
color, w = QColor(tok.text_muted), 3
|
||||
else:
|
||||
color, w = QColor("#3A5A78"), 2
|
||||
color, w = QColor(tok.border_strong), 2
|
||||
self.setPen(QPen(color, w, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin))
|
||||
|
||||
def update_path(self, points):
|
||||
@@ -491,7 +504,8 @@ class Co4ECanvas(QGraphicsView):
|
||||
self._port_src_pt = scene_pt
|
||||
self._temp_edge = QGraphicsPathItem()
|
||||
self._temp_edge.setZValue(3.5) # above nodes + edges while connecting
|
||||
self._temp_edge.setPen(QPen(QColor("#48CAE4"), 2, Qt.DashLine, Qt.RoundCap))
|
||||
self._temp_edge.setPen(
|
||||
QPen(QColor(current_palette().accent), 2, Qt.DashLine, Qt.RoundCap))
|
||||
self._scene.addItem(self._temp_edge)
|
||||
|
||||
def update_port_drag(self, scene_pt: QPointF) -> None:
|
||||
|
||||
+12
-8
@@ -34,6 +34,7 @@ from ..core.co4e_builtins import BUILTIN_AGENTS
|
||||
from ..core.co4e_run_manager import Co4ERunManager
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..theme import current_palette
|
||||
from .chat_view import ChatView
|
||||
from .co4e_canvas import CO4E_MIME, Co4ECanvas
|
||||
from .co4e_config_panel import StepConfigPanel
|
||||
@@ -564,13 +565,14 @@ class Co4ETab(QWidget):
|
||||
# Tab colours + layout live in theme.py (QTabBar#flowTabs — theme-aware,
|
||||
# flush, centred). Here we only style the per-tab close (✕) button, which
|
||||
# QTabBar places centred on the tab's right (see _add_tab_close_button).
|
||||
_fp = current_palette()
|
||||
self.flow_bar.setStyleSheet(
|
||||
"QPushButton#flowTabClose {"
|
||||
" border: none; background: transparent; color: #8FB2D4;"
|
||||
f" border: none; background: transparent; color: {_fp.text_muted};"
|
||||
" font-size: 13px; font-weight: bold; padding: 0; margin: 0;"
|
||||
" border-radius: 8px; }"
|
||||
f" border-radius: {_fp.radius_sm}px; }}"
|
||||
"QPushButton#flowTabClose:hover {"
|
||||
" background: rgba(229,72,77,0.18); color: #E5484D; }")
|
||||
f" background: {_fp.danger_soft}; color: {_fp.danger}; }}")
|
||||
runs_idx = self.flow_bar.addTab(icon("monitoring"), tr("co4e.runs_tab")) # 0 = Runs
|
||||
self.flow_bar.setTabButton(runs_idx, QTabBar.RightSide, None) # pinned
|
||||
self.flow_bar.currentChanged.connect(self._on_flow_tab_changed)
|
||||
@@ -606,7 +608,7 @@ class Co4ETab(QWidget):
|
||||
"QScrollArea#flowTabScroll { background: transparent; border: none; }"
|
||||
"QScrollArea#flowTabScroll QScrollBar:horizontal { height: 8px; background: transparent; margin: 0; }"
|
||||
"QScrollArea#flowTabScroll QScrollBar::handle:horizontal {"
|
||||
" background: rgba(143,178,212,0.45); border-radius: 4px; min-width: 30px; }"
|
||||
f" background: {current_palette().scroll_handle}; border-radius: 4px; min-width: 30px; }}"
|
||||
"QScrollArea#flowTabScroll QScrollBar::add-line:horizontal,"
|
||||
"QScrollArea#flowTabScroll QScrollBar::sub-line:horizontal { width: 0; height: 0; }")
|
||||
flow_row = QHBoxLayout()
|
||||
@@ -861,7 +863,8 @@ class Co4ETab(QWidget):
|
||||
# $cost) at the bottom, exactly like Cowork's conversation total.
|
||||
self._usage_total_lbl = QLabel("")
|
||||
self._usage_total_lbl.setObjectName("hint")
|
||||
self._usage_total_lbl.setStyleSheet("color: rgba(140,146,152,0.9); font-size: 11px;")
|
||||
self._usage_total_lbl.setStyleSheet(
|
||||
f"color: {current_palette().text_faint}; font-size: 11px;")
|
||||
crow.addWidget(self._usage_total_lbl)
|
||||
_inp = QWidget(); row = QHBoxLayout(_inp); row.setContentsMargins(0, 0, 0, 0)
|
||||
self.chat_input = _ChatInput()
|
||||
@@ -1331,8 +1334,9 @@ class Co4ETab(QWidget):
|
||||
# Rebuild the always-fresh Runs table from the manager (single source of truth).
|
||||
if not hasattr(self, "runs_table"):
|
||||
return
|
||||
color = {"running": "#48CAE4", "done": "#48D9A0", "error": "#E5484D",
|
||||
"stopped": "#8FB2D4"}
|
||||
p = current_palette()
|
||||
color = {"running": p.accent, "done": p.success, "error": p.danger,
|
||||
"stopped": p.text_muted}
|
||||
dots = {"running": "▶", "done": "✓", "error": "✕", "stopped": "■"}
|
||||
# Most-recent run at the TOP, oldest at the bottom (manager keeps runs in
|
||||
# chronological insertion order, so reverse it for display).
|
||||
@@ -1352,7 +1356,7 @@ class Co4ETab(QWidget):
|
||||
if c == 0:
|
||||
it.setData(Qt.UserRole, h.id)
|
||||
if c == 1:
|
||||
it.setForeground(_qcolor(color.get(h.status, "#E0F0FF")))
|
||||
it.setForeground(_qcolor(color.get(h.status, p.text)))
|
||||
t.setItem(r, c, it)
|
||||
if h.id == sel_id:
|
||||
sel_row = r
|
||||
|
||||
+5
-1
@@ -20,6 +20,7 @@ from PySide6.QtWidgets import (
|
||||
|
||||
from ..config import CONFIG_DIR
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..theme import current_palette
|
||||
from .icons import icon, IconLabel
|
||||
|
||||
|
||||
@@ -583,7 +584,10 @@ class Composer(QWidget):
|
||||
for p in self._attachments:
|
||||
item = QListWidgetItem()
|
||||
row = QWidget()
|
||||
row.setStyleSheet("background: rgba(140,146,152,0.18); border-radius: 6px;")
|
||||
_cp = current_palette()
|
||||
row.setStyleSheet(
|
||||
f"background: {_cp.surface_raised}; border: 1px solid {_cp.border};"
|
||||
f" border-radius: {_cp.radius_sm}px;")
|
||||
h = QHBoxLayout(row)
|
||||
h.setContentsMargins(8, 2, 4, 2)
|
||||
h.setSpacing(4)
|
||||
|
||||
+5
-1
@@ -24,6 +24,7 @@ from ..core import usage_tracker as ut
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..state import AppContext
|
||||
from ..theme import current_palette
|
||||
from .icons import icon
|
||||
from .spline_chart import SplineChart
|
||||
from .widgets import BudgetCard as _BudgetCard
|
||||
@@ -298,8 +299,11 @@ class DashboardTab(QWidget):
|
||||
n_points = max(1, len(parts))
|
||||
refs = []
|
||||
if prev[mi] > 0:
|
||||
# Muted on purpose: the comparison line is a reference, not the
|
||||
# series — it must not compete with the accent-coloured spline.
|
||||
refs.append((prev[mi] / n_points,
|
||||
f"{tr(ref_key)} {self._delta_txt(cur[mi], prev[mi])}", "#B08968"))
|
||||
f"{tr(ref_key)} {self._delta_txt(cur[mi], prev[mi])}",
|
||||
current_palette().text_muted))
|
||||
self.chart.set_reference_lines(refs)
|
||||
self.chart.set_data(pts, fmt, tr(f"dashboard.metric_{metric}"))
|
||||
self._chart_period_lbl.setText(ut.period_range_label(gran, self._chart_offset))
|
||||
|
||||
+27
-24
@@ -33,6 +33,7 @@ from PySide6.QtWidgets import (
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..state import AppContext
|
||||
from ..theme import current_palette
|
||||
from .chat_view import ChatView
|
||||
from .icons import icon
|
||||
from .libreoffice_view import DOC_SUFFIXES
|
||||
@@ -89,23 +90,26 @@ class PygmentsHighlighter(QSyntaxHighlighter):
|
||||
from pygments.token import (
|
||||
Comment, Error, Keyword, Name, Number, Operator, Punctuation, String,
|
||||
)
|
||||
p = current_palette()
|
||||
# Ordered specific → general: first matching token type wins.
|
||||
# Colours are resolved when the editor is built, so reopening a file
|
||||
# after a theme switch re-highlights it in the new theme.
|
||||
return [
|
||||
(Comment, _fmt("#6A9955", italic=True)),
|
||||
(Keyword.Type, _fmt("#4EC9B0")),
|
||||
(Keyword, _fmt("#569CD6")),
|
||||
(Name.Function, _fmt("#DCDCAA")),
|
||||
(Name.Class, _fmt("#4EC9B0")),
|
||||
(Name.Decorator, _fmt("#DCDCAA")),
|
||||
(Name.Builtin, _fmt("#4EC9B0")),
|
||||
(Name.Tag, _fmt("#569CD6")),
|
||||
(Name.Attribute, _fmt("#9CDCFE")),
|
||||
(String.Doc, _fmt("#6A9955", italic=True)),
|
||||
(String, _fmt("#CE9178")),
|
||||
(Number, _fmt("#B5CEA8")),
|
||||
(Operator, _fmt("#D4D4D4")),
|
||||
(Punctuation, _fmt("#D4D4D4")),
|
||||
(Error, _fmt("#F44747")),
|
||||
(Comment, _fmt(p.code_comment, italic=True)),
|
||||
(Keyword.Type, _fmt(p.code_type)),
|
||||
(Keyword, _fmt(p.code_keyword)),
|
||||
(Name.Function, _fmt(p.code_func)),
|
||||
(Name.Class, _fmt(p.code_type)),
|
||||
(Name.Decorator, _fmt(p.code_func)),
|
||||
(Name.Builtin, _fmt(p.code_type)),
|
||||
(Name.Tag, _fmt(p.code_keyword)),
|
||||
(Name.Attribute, _fmt(p.code_attr)),
|
||||
(String.Doc, _fmt(p.code_comment, italic=True)),
|
||||
(String, _fmt(p.code_string)),
|
||||
(Number, _fmt(p.code_number)),
|
||||
(Operator, _fmt(p.code_fg)),
|
||||
(Punctuation, _fmt(p.code_fg)),
|
||||
(Error, _fmt(p.code_error)),
|
||||
]
|
||||
|
||||
def set_filename(self, filename: str, text: str = "") -> None:
|
||||
@@ -179,9 +183,7 @@ class CodeEditor(QPlainTextEdit):
|
||||
font.setStyleHint(QFont.Monospace)
|
||||
font.setPointSize(10)
|
||||
self.setFont(font)
|
||||
self.setStyleSheet(
|
||||
"#codeEditor { background: #1e1e1e; color: #d4d4d4; border: none; "
|
||||
"selection-background-color: #264f78; }")
|
||||
# Surface comes from the central style sheet (#codeEditor) — see theme.py.
|
||||
self._gutter = _LineNumbers(self)
|
||||
self.blockCountChanged.connect(lambda _=0: self._update_gutter_width())
|
||||
self.updateRequest.connect(self._on_update_request)
|
||||
@@ -210,13 +212,14 @@ class CodeEditor(QPlainTextEdit):
|
||||
self._gutter.setGeometry(QRect(cr.left(), cr.top(), self.line_number_width(), cr.height()))
|
||||
|
||||
def paint_line_numbers(self, event) -> None:
|
||||
p = current_palette()
|
||||
painter = QPainter(self._gutter)
|
||||
painter.fillRect(event.rect(), QColor("#1a1a1a"))
|
||||
painter.fillRect(event.rect(), QColor(p.code_gutter_bg))
|
||||
block = self.firstVisibleBlock()
|
||||
num = block.blockNumber()
|
||||
top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
|
||||
bottom = top + self.blockBoundingRect(block).height()
|
||||
painter.setPen(QColor("#858585"))
|
||||
painter.setPen(QColor(p.code_gutter_fg))
|
||||
while block.isValid() and top <= event.rect().bottom():
|
||||
if block.isVisible() and bottom >= event.rect().top():
|
||||
painter.drawText(0, int(top), self._gutter.width() - 6,
|
||||
@@ -1096,7 +1099,7 @@ class FolderTab(QWidget):
|
||||
if n and hasattr(self, "_ai_status"):
|
||||
self._ai_status.setText("⏳ " + tr("folder.ai_status_running")
|
||||
+ " · " + tr("folder.ai_queue_count", n=n))
|
||||
self._ai_status.setStyleSheet("color:#0096C7;")
|
||||
self._ai_status.setStyleSheet(f"color:{current_palette().accent};")
|
||||
|
||||
def _ai_maybe_dequeue(self) -> None:
|
||||
"""When the pipeline is fully idle, start the next queued instruction."""
|
||||
@@ -1308,7 +1311,7 @@ class FolderTab(QWidget):
|
||||
name = target if create else getattr(self, "_ai_running_file", "")
|
||||
self.status_message.emit(tr("folder.ai_proposed_status", name=name))
|
||||
self._ai_status.setText("● " + hint)
|
||||
self._ai_status.setStyleSheet("color:#c77d00;")
|
||||
self._ai_status.setStyleSheet(f"color:{current_palette().warning};")
|
||||
|
||||
def _ai_apply(self) -> None:
|
||||
"""Confirmed by the user. If the edit GENERATES images, ask the image
|
||||
@@ -1464,7 +1467,7 @@ class FolderTab(QWidget):
|
||||
self.ai_send_btn.setEnabled(not busy)
|
||||
if busy:
|
||||
self._ai_status.setText("⏳ " + tr("folder.ai_status_running"))
|
||||
self._ai_status.setStyleSheet("color:#0096C7;")
|
||||
self._ai_status.setStyleSheet(f"color:{current_palette().accent};")
|
||||
self.ai_btn.setText(tr("folder.ai_edit") + " ⏳") # visible even when collapsed
|
||||
else:
|
||||
self._ai_status.setText("")
|
||||
@@ -1478,7 +1481,7 @@ class FolderTab(QWidget):
|
||||
self._ai_maybe_dequeue()
|
||||
return
|
||||
self._ai_status.setText("✓ " + tr("folder.ai_status_done"))
|
||||
self._ai_status.setStyleSheet("color:#1f9d63;")
|
||||
self._ai_status.setStyleSheet(f"color:{current_palette().success};")
|
||||
if not self.ai_btn.isChecked() or self._ai_panel.isHidden():
|
||||
self.ai_btn.setText(tr("folder.ai_edit") + " ✓")
|
||||
|
||||
|
||||
+50
-57
@@ -103,67 +103,58 @@ class HelpAgentWidget(QWidget):
|
||||
self._apply_state()
|
||||
|
||||
# ---- theming ----------------------------------------------------------
|
||||
def _compute_palette(self) -> Dict[str, str]:
|
||||
"""Chat-body colours that FOLLOW the app's light/dark theme. The header
|
||||
is intentionally NOT themed here (it stays a fixed light bar — see
|
||||
_apply_style), only the conversation area adapts."""
|
||||
from ..theme import resolve_theme
|
||||
dark = resolve_theme(getattr(self.ctx.config, "theme", "system")) == "dark"
|
||||
if dark:
|
||||
return {
|
||||
"panel_bg": "#16202b", "text": "#e3ebf5", "log_bg": "#0f1720",
|
||||
"input_bg": "#1b2733", "border": "#33404d",
|
||||
"user_bg": "#123a52", "user_label": "#58c0ee",
|
||||
"bot_bg": "#232f3b", "bot_label": "#6fe3a4",
|
||||
}
|
||||
return {
|
||||
"panel_bg": "#ffffff", "text": "#14212b", "log_bg": "#f7f9fb",
|
||||
"input_bg": "#ffffff", "border": "#d5d9de",
|
||||
"user_bg": "#dceff8", "user_label": "#0077B6",
|
||||
"bot_bg": "#eef1f4", "bot_label": "#2f7d55",
|
||||
}
|
||||
def _compute_palette(self):
|
||||
"""The app's design tokens for the theme in effect. The whole dock —
|
||||
header included — follows the app theme; a header locked to a light
|
||||
strip stranded a bright bar in the middle of the dark UI."""
|
||||
from ..theme import palette
|
||||
return palette(getattr(self.ctx.config, "theme", "system"))
|
||||
|
||||
def apply_theme(self) -> None:
|
||||
"""Re-style + re-render when the app theme switches (called from
|
||||
MainWindow._apply_theme). Header stays fixed; chat body re-colours."""
|
||||
MainWindow._apply_theme). The whole dock re-colours, icons included —
|
||||
icons are painted bitmaps, so they must be rebuilt, not restyled."""
|
||||
self._pal = self._compute_palette()
|
||||
self._apply_style()
|
||||
muted = self._pal.text_muted
|
||||
self.edge_tab.setIcon(icon("chevron-left", color=muted))
|
||||
self.collapse_btn.setIcon(icon("chevron-right", color=muted))
|
||||
self.min_btn.setIcon(icon("minus", color=muted))
|
||||
self._render()
|
||||
|
||||
def _apply_style(self) -> None:
|
||||
# The HEADER bar is a FIXED light strip in both themes (per request); only
|
||||
# the chat body below follows the app's light/dark palette (self._pal).
|
||||
from ..theme import ACCENT, ACCENT2, GRADIENT
|
||||
"""The dock owns its own style sheet (it floats above the window, so the
|
||||
app-wide sheet does not reach it cleanly) but draws every value from the
|
||||
shared tokens — see theme.py."""
|
||||
p = self._pal
|
||||
r, rl = p.radius, p.radius_lg
|
||||
self.setStyleSheet(f"""
|
||||
/* Clean rounded app-icon badge (like image 2): a fixed light card
|
||||
framing the icon — no QPushButton box. */
|
||||
#helpLauncher {{ background: #e8f2fb; border: 1px solid #d3e3f2;
|
||||
border-radius: 16px; }}
|
||||
#helpLauncher:hover {{ background: #dcedfb; }}
|
||||
#helpCollapseBtn, #helpEdgeTab {{ background: rgba(0,0,0,0.06); border: none;
|
||||
border-radius: 6px; }}
|
||||
#helpCollapseBtn:hover, #helpEdgeTab:hover {{ background: rgba(0,0,0,0.14); }}
|
||||
#helpPanel {{ background: {p['panel_bg']}; border: 1px solid {p['border']};
|
||||
border-radius: 14px; color: {p['text']}; }}
|
||||
/* Faint-blue header bar — LOCKED light, dark title, in both themes.
|
||||
The header AND its child labels set fixed backgrounds so the dark
|
||||
theme never bleeds into the App-Assistant title strip. */
|
||||
#helpHeader {{ background: #e8f2fb; border-bottom: 1px solid #d9e6f2;
|
||||
border-top-left-radius: 14px; border-top-right-radius: 14px; }}
|
||||
#helpHeader QLabel {{ background: transparent; color: #14212b; }}
|
||||
#helpTitle {{ color: #14212b; font-weight: 700; font-size: 13px; background: transparent; }}
|
||||
#helpMinBtn {{ background: transparent; border: none; }}
|
||||
#helpMinBtn:hover {{ background: rgba(0,0,0,0.10); border-radius: 6px; }}
|
||||
#helpLog {{ background: {p['log_bg']}; border: none; color: {p['text']}; padding: 4px 6px; }}
|
||||
#helpInputRow {{ background: {p['panel_bg']}; border-bottom-left-radius: 14px;
|
||||
border-bottom-right-radius: 14px; }}
|
||||
#helpInput {{ border: 1px solid {p['border']}; border-radius: 8px; padding: 5px 8px;
|
||||
background: {p['input_bg']}; color: {p['text']}; }}
|
||||
#helpInput:focus {{ border: 1px solid {ACCENT}; }}
|
||||
#helpSendBtn {{ background: {GRADIENT}; border: none; border-radius: 8px; }}
|
||||
#helpSendBtn:hover {{ background: {ACCENT2}; }}
|
||||
#helpSendBtn:disabled {{ background: #b7c0c9; }}
|
||||
/* The app-icon badge that opens the dock: a plain card, no button box. */
|
||||
#helpLauncher {{ background: {p.surface}; border: 1px solid {p.border};
|
||||
border-radius: {rl}px; }}
|
||||
#helpLauncher:hover {{ background: {p.hover}; }}
|
||||
#helpCollapseBtn, #helpEdgeTab {{ background: {p.surface}; border: none;
|
||||
border-radius: {r}px; }}
|
||||
#helpCollapseBtn:hover, #helpEdgeTab:hover {{ background: {p.hover}; }}
|
||||
#helpPanel {{ background: {p.surface}; border: 1px solid {p.border};
|
||||
border-radius: {rl}px; color: {p.text}; }}
|
||||
#helpHeader {{ background: {p.surface}; border-bottom: 1px solid {p.border};
|
||||
border-top-left-radius: {rl}px; border-top-right-radius: {rl}px; }}
|
||||
#helpHeader QLabel {{ background: transparent; color: {p.text}; }}
|
||||
#helpTitle {{ color: {p.text}; font-weight: 600; font-size: 13px;
|
||||
background: transparent; }}
|
||||
#helpMinBtn {{ background: transparent; border: none; border-radius: {r}px; }}
|
||||
#helpMinBtn:hover {{ background: {p.hover}; }}
|
||||
#helpLog {{ background: {p.sunken}; border: none; color: {p.text};
|
||||
padding: 4px 6px; }}
|
||||
#helpInputRow {{ background: {p.surface};
|
||||
border-bottom-left-radius: {rl}px; border-bottom-right-radius: {rl}px; }}
|
||||
#helpInput {{ border: 1px solid {p.border}; border-radius: {r}px; padding: 5px 8px;
|
||||
background: {p.surface_raised}; color: {p.text}; }}
|
||||
#helpInput:focus {{ border: 1px solid {p.focus_ring}; }}
|
||||
#helpSendBtn {{ background: {p.accent_solid}; border: none; border-radius: {r}px; }}
|
||||
#helpSendBtn:hover {{ background: {p.accent_solid_hover}; }}
|
||||
#helpSendBtn:disabled {{ background: {p.border_strong}; }}
|
||||
""")
|
||||
|
||||
# ---- greeting / labels ------------------------------------------------
|
||||
@@ -177,7 +168,7 @@ class HelpAgentWidget(QWidget):
|
||||
# assistant back (chevron points left = "slide out").
|
||||
self.edge_tab = QPushButton(self)
|
||||
self.edge_tab.setObjectName("helpEdgeTab")
|
||||
self.edge_tab.setIcon(icon("chevron-left", color="#5a6570"))
|
||||
self.edge_tab.setIcon(icon("chevron-left", color=self._pal.text_muted))
|
||||
self.edge_tab.setCursor(Qt.PointingHandCursor)
|
||||
self.edge_tab.setToolTip(tr("help_agent.show_tooltip"))
|
||||
self.edge_tab.clicked.connect(self._show_launcher)
|
||||
@@ -186,7 +177,7 @@ class HelpAgentWidget(QWidget):
|
||||
# A left-side chevron collapses the assistant to the edge…
|
||||
self.collapse_btn = QPushButton(self)
|
||||
self.collapse_btn.setObjectName("helpCollapseBtn")
|
||||
self.collapse_btn.setIcon(icon("chevron-right", color="#5a6570"))
|
||||
self.collapse_btn.setIcon(icon("chevron-right", color=self._pal.text_muted))
|
||||
self.collapse_btn.setCursor(Qt.PointingHandCursor)
|
||||
self.collapse_btn.setToolTip(tr("help_agent.hide_tooltip"))
|
||||
self.collapse_btn.clicked.connect(self._hide_to_edge)
|
||||
@@ -222,7 +213,7 @@ class HelpAgentWidget(QWidget):
|
||||
hb.addWidget(self.title, 1)
|
||||
self.min_btn = QPushButton(header)
|
||||
self.min_btn.setObjectName("helpMinBtn")
|
||||
self.min_btn.setIcon(icon("minus", color="#5a6570"))
|
||||
self.min_btn.setIcon(icon("minus", color=self._pal.text_muted))
|
||||
self.min_btn.setFixedSize(24, 24)
|
||||
self.min_btn.setCursor(Qt.PointingHandCursor)
|
||||
self.min_btn.setToolTip(tr("help_agent.collapse_tooltip"))
|
||||
@@ -318,17 +309,19 @@ class HelpAgentWidget(QWidget):
|
||||
p = self._pal
|
||||
text = (content or "").replace("&", "&").replace("<", "<").replace(">", ">")
|
||||
text = text.replace("\n", "<br>")
|
||||
# bgcolor= is a solid-only HTML attribute, hence accent_wash (pre-blended)
|
||||
# rather than the translucent accent_soft used in style sheets.
|
||||
if who == "user":
|
||||
align, bg, label_color = "right", p["user_bg"], p["user_label"]
|
||||
align, bg, label_color = "right", p.accent_wash, p.accent
|
||||
label = tr("chat.you")
|
||||
else:
|
||||
align, bg, label_color = "left", p["bot_bg"], p["bot_label"]
|
||||
align, bg, label_color = "left", p.surface_raised, p.success
|
||||
label = tr("help_agent.title")
|
||||
return (
|
||||
f'<table width="100%" cellspacing="0" cellpadding="0"><tr>'
|
||||
f'<td align="{align}">'
|
||||
f'<table width="80%" cellspacing="0" cellpadding="7" bgcolor="{bg}"><tr>'
|
||||
f'<td style="color:{p["text"]};">'
|
||||
f'<td style="color:{p.text};">'
|
||||
f'<b style="color:{label_color};">{label}</b><br>{text}'
|
||||
f'</td></tr></table></td></tr></table>'
|
||||
'<div style="line-height:6px;"> </div>' # gap between turns
|
||||
|
||||
+24
-12
@@ -21,7 +21,12 @@ from PySide6.QtGui import QBrush, QColor, QIcon, QPainter, QPen, QPixmap
|
||||
from PySide6.QtSvg import QSvgRenderer
|
||||
from PySide6.QtWidgets import QComboBox, QHBoxLayout, QLabel, QWidget
|
||||
|
||||
_COLOR = "#8b8d98" # neutral grey, visible on both light and dark buttons
|
||||
def _default_color() -> str:
|
||||
"""The default icon tint: the theme's muted text colour, so glyphs sit at
|
||||
the same weight as the labels beside them. Resolved per call — icons are
|
||||
painted bitmaps, so a theme switch must repaint them, not restyle them."""
|
||||
from ..theme import current_palette
|
||||
return current_palette().text_muted
|
||||
|
||||
|
||||
def _hidpi_pixmap(size: int) -> QPixmap:
|
||||
@@ -230,10 +235,11 @@ def icon_picker_combo(current: str = "") -> QComboBox:
|
||||
return combo
|
||||
|
||||
|
||||
def icon(name: str, size: int = 16, color: str = _COLOR) -> QIcon:
|
||||
def icon(name: str, size: int = 16, color: str | None = None) -> QIcon:
|
||||
"""A flat thin-line icon for ``name`` (see ``_PATHS`` for the full list),
|
||||
tinted ``color`` — rendered from local SVG data, no image files/network.
|
||||
Stroke width 1.7 matches the Nova Platform web app's shared icon set."""
|
||||
color = color or _default_color()
|
||||
# A user-added custom icon (full SVG under ~/.cowork_local/icons) is rendered
|
||||
# as-is (keeps its own colours). Then built-in glyphs; then a neutral fallback.
|
||||
if name not in _PATHS:
|
||||
@@ -264,9 +270,10 @@ def icon(name: str, size: int = 16, color: str = _COLOR) -> QIcon:
|
||||
return QIcon(pm)
|
||||
|
||||
|
||||
def _panel_icon(fill_left: bool, size: int = 16, color: str = _COLOR) -> QIcon:
|
||||
def _panel_icon(fill_left: bool, size: int = 16, color: str | None = None) -> QIcon:
|
||||
"""A rounded panel split by a divider, with one narrow side filled solid
|
||||
(the 'sidebar' toggle look)."""
|
||||
color = color or _default_color()
|
||||
pm = _hidpi_pixmap(size)
|
||||
p = QPainter(pm)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
@@ -302,19 +309,24 @@ def collapse_right_icon() -> QIcon:
|
||||
return _panel_icon(fill_left=False)
|
||||
|
||||
|
||||
def pixmap(name: str, size: int = 16, color: str = _COLOR) -> QPixmap:
|
||||
def pixmap(name: str, size: int = 16, color: str | None = None) -> QPixmap:
|
||||
"""The line-icon ``name`` as a QPixmap (for QLabel.setPixmap — QLabel has no
|
||||
setIcon). Same glyph/renderer as ``icon()``."""
|
||||
return icon(name, size, color).pixmap(size, size)
|
||||
|
||||
|
||||
# Status-LED colors — a filled dot, the one place a solid glyph (not a line
|
||||
# Status-LED colours — a filled dot, the one place a solid glyph (not a line
|
||||
# icon) is the right metaphor for an on/off/running indicator.
|
||||
DOT_GREEN = "#22c55e"
|
||||
DOT_RED = "#ef4444"
|
||||
DOT_AMBER = "#f59e0b"
|
||||
DOT_BLUE = "#3b82f6"
|
||||
DOT_GREY = "#9ca3af"
|
||||
#
|
||||
# Deliberately the SAME in light and dark. An LED means one thing regardless of
|
||||
# theme, and these mid-saturation hues clear 3:1 against both #0B0B0C and
|
||||
# #FFFFFF, so a status dot never has to be re-learned. Everything else in the
|
||||
# UI goes through theme.palette(); this is the documented exception.
|
||||
DOT_GREEN = "#2EA043"
|
||||
DOT_RED = "#E5484D"
|
||||
DOT_AMBER = "#B7791F"
|
||||
DOT_BLUE = "#4C7BE8"
|
||||
DOT_GREY = "#8B8B94"
|
||||
|
||||
|
||||
def dot_icon(color: str = DOT_GREY, size: int = 12) -> QIcon:
|
||||
@@ -338,7 +350,7 @@ class IconLabel(QWidget):
|
||||
status labels (lock/unlock, …) keep working."""
|
||||
|
||||
def __init__(self, name: str, text: str = "", *, size: int = 16,
|
||||
color: str = _COLOR, gap: int = 6, parent=None):
|
||||
color: str | None = None, gap: int = 6, parent=None):
|
||||
super().__init__(parent)
|
||||
self._size = size
|
||||
lay = QHBoxLayout(self)
|
||||
@@ -357,7 +369,7 @@ class IconLabel(QWidget):
|
||||
def setText(self, text: str) -> None: # noqa: N802 — QLabel-compatible alias
|
||||
self._text.setText(text)
|
||||
|
||||
def set_icon(self, name: str, color: str = _COLOR) -> None:
|
||||
def set_icon(self, name: str, color: str | None = None) -> None:
|
||||
self._icon.setPixmap(pixmap(name, self._size, color))
|
||||
|
||||
def text_label(self) -> QLabel:
|
||||
|
||||
@@ -36,6 +36,7 @@ from ..core import agent_roles, audit_log
|
||||
from ..core import usage_tracker as ut
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..state import AppContext
|
||||
from ..theme import current_palette
|
||||
from .icons import icon, DOT_GREEN, DOT_RED, DOT_AMBER
|
||||
from .widgets import BudgetCard, StatCard, fmt_tokens
|
||||
|
||||
@@ -801,7 +802,8 @@ class MonitoringTab(QWidget):
|
||||
mark = f"<span style='color:{DOT_RED};'>✗</span>"
|
||||
name = event.get("name", "") or event.get("kind", "")
|
||||
rel = _relative_time(event.get("ts", ""))
|
||||
suffix = f" <span style='color:#8b8d98;'>— {rel}</span>" if rel else ""
|
||||
muted = current_palette().text_muted
|
||||
suffix = f" <span style='color:{muted};'>— {rel}</span>" if rel else ""
|
||||
return f"{mark} {name}{suffix}"
|
||||
|
||||
def _refresh_usage_cards(self) -> None:
|
||||
|
||||
+719
-716
File diff suppressed because it is too large
Load Diff
+648
-649
File diff suppressed because it is too large
Load Diff
+5
-9
@@ -13,7 +13,7 @@ from PySide6.QtCore import QPointF, Qt
|
||||
from PySide6.QtGui import QBrush, QColor, QLinearGradient, QPainter, QPainterPath, QPen
|
||||
from PySide6.QtWidgets import QWidget
|
||||
|
||||
from ..theme import ACCENT
|
||||
from ..theme import current_palette
|
||||
|
||||
|
||||
def _endpoint_label_rect(point_x: float, point_y: float, text_width: float,
|
||||
@@ -75,17 +75,13 @@ class SplineChart(QWidget):
|
||||
self._refs = list(refs or [])
|
||||
self.update()
|
||||
|
||||
def _dark(self) -> bool:
|
||||
from .chat_view import _app_theme
|
||||
return _app_theme() == "dark"
|
||||
|
||||
def paintEvent(self, _e): # noqa: N802
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
dark = self._dark()
|
||||
grid = QColor("#1A2D4A" if dark else "#C7DEEE")
|
||||
text = QColor("#8FB2D4" if dark else "#5C7A94")
|
||||
accent = QColor(ACCENT)
|
||||
tok = current_palette()
|
||||
grid = QColor(tok.chart_grid)
|
||||
text = QColor(tok.chart_label)
|
||||
accent = QColor(tok.accent)
|
||||
w, h = self.width(), self.height()
|
||||
|
||||
pts = self._points
|
||||
|
||||
+998
-993
File diff suppressed because it is too large
Load Diff
@@ -75,12 +75,13 @@ class TaskEditorDialog(QDialog):
|
||||
# with a lighter box) — just a light outline, consistent with the rest of
|
||||
# the app. The combo drop-down popup keeps a solid dark background so its
|
||||
# items stay readable.
|
||||
# Inputs in this dense form sit flat on the dialog rather than on their
|
||||
# own raised surface — the app-wide sheet styles everything else here,
|
||||
# including the combo popup, so nothing needs a colour override.
|
||||
self.setStyleSheet(
|
||||
"QLineEdit, QPlainTextEdit, QListWidget, QComboBox, QAbstractSpinBox {"
|
||||
" background: transparent; border: 1px solid rgba(140,146,152,0.45);"
|
||||
" border-radius: 6px; }"
|
||||
"QListWidget::item { background: transparent; }"
|
||||
"QComboBox QAbstractItemView { background: #111D32; color: #E0F0FF; }")
|
||||
" background: transparent; }"
|
||||
"QListWidget::item { background: transparent; }")
|
||||
|
||||
outer = QVBoxLayout(self)
|
||||
scroll = QScrollArea()
|
||||
|
||||
@@ -27,6 +27,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
|
||||
from ..i18n import on_language_changed, tr
|
||||
from ..theme import current_palette
|
||||
from .icons import icon
|
||||
|
||||
_IS_WIN = sys.platform == "win32"
|
||||
@@ -75,8 +76,10 @@ class TerminalPanel(QWidget):
|
||||
# ---- header (always visible; click to expand/collapse) --------------
|
||||
self._header = QFrame()
|
||||
self._header.setObjectName("termHeader")
|
||||
_tp = current_palette()
|
||||
self._header.setStyleSheet(
|
||||
"#termHeader { background: rgba(0,0,0,0.06); border-radius: 6px; }")
|
||||
f"#termHeader {{ background: {_tp.surface};"
|
||||
f" border-radius: {_tp.radius}px; }}")
|
||||
hb = QHBoxLayout(self._header)
|
||||
hb.setContentsMargins(8, 4, 8, 4)
|
||||
self._toggle_btn = QPushButton()
|
||||
@@ -107,8 +110,7 @@ class TerminalPanel(QWidget):
|
||||
mono.setStyleHint(QFont.Monospace)
|
||||
mono.setPointSize(10)
|
||||
self.output.setFont(mono)
|
||||
self.output.setStyleSheet(
|
||||
"#termOutput { background: #1e1e1e; color: #d4d4d4; border: none; }")
|
||||
# Surface comes from the central style sheet (#termOutput) — see theme.py.
|
||||
self.output.setMinimumHeight(160)
|
||||
bl.addWidget(self.output, 1)
|
||||
|
||||
@@ -119,9 +121,7 @@ class TerminalPanel(QWidget):
|
||||
self.input = _TermInput()
|
||||
self.input.setObjectName("termInput")
|
||||
self.input.setFont(mono)
|
||||
self.input.setStyleSheet(
|
||||
"#termInput { background: #1e1e1e; color: #d4d4d4; border: 1px solid #3c3c3c; "
|
||||
"border-radius: 6px; padding: 4px 8px; }")
|
||||
# Surface comes from the central style sheet (#termInput) — see theme.py.
|
||||
self.input.returnPressed.connect(self._run_current)
|
||||
self.input.complete_requested.connect(self._complete)
|
||||
self.input.history_prev.connect(lambda: self._history_move(-1))
|
||||
@@ -282,11 +282,12 @@ class TerminalPanel(QWidget):
|
||||
if not text:
|
||||
return
|
||||
from PySide6.QtGui import QColor, QTextCursor
|
||||
colors = {"cmd": "#4ec9b0", "err": "#f48771", "ok": "#6a9955", "out": "#d4d4d4"}
|
||||
p = current_palette()
|
||||
colors = {"cmd": p.code_type, "err": p.code_error, "ok": p.code_comment, "out": p.code_fg}
|
||||
cursor = self.output.textCursor()
|
||||
cursor.movePosition(QTextCursor.End)
|
||||
fmt = cursor.charFormat()
|
||||
fmt.setForeground(QColor(colors.get(role, "#d4d4d4")))
|
||||
fmt.setForeground(QColor(colors.get(role, p.code_fg)))
|
||||
cursor.setCharFormat(fmt)
|
||||
cursor.insertText(text)
|
||||
self.output.setTextCursor(cursor)
|
||||
|
||||
+32
-28
@@ -8,36 +8,40 @@ from pathlib import Path
|
||||
from PySide6.QtCore import QEvent, QObject, QPointF, QRectF, Qt, Signal
|
||||
from PySide6.QtGui import QColor, QPainter, QPen
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractSpinBox, QComboBox, QDoubleSpinBox, QFrame, QGraphicsDropShadowEffect,
|
||||
QAbstractSpinBox, QComboBox, QDoubleSpinBox, QFrame,
|
||||
QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QSizePolicy,
|
||||
QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..core.flows import STEP_DONE, STEP_ERROR, STEP_PENDING, STEP_RUNNING
|
||||
from ..theme import ACCENT
|
||||
from ..theme import current_palette
|
||||
from .icons import DOT_BLUE, DOT_GREEN, DOT_GREY, DOT_RED, dot_icon, icon
|
||||
|
||||
|
||||
def _style_card(frame: QFrame) -> None:
|
||||
"""Give a stat/budget card its surface. Flat by design: the raised surface
|
||||
plus a hairline is what separates it from the page — the old drop shadow
|
||||
made a grid of these look like it was hovering off the screen."""
|
||||
p = current_palette()
|
||||
frame.setStyleSheet(
|
||||
f"QFrame {{ background: {p.surface}; border: 1px solid {p.border};"
|
||||
f" border-radius: {p.radius_lg}px; }}")
|
||||
|
||||
|
||||
class StatCard(QFrame):
|
||||
"""A titled value card (e.g. token count + its cost as the subtitle) —
|
||||
shared by Dashboard and Monitoring's token/cost displays."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setStyleSheet(
|
||||
"QFrame { border: 1px solid rgba(140,146,152,0.35); border-radius: 16px; }")
|
||||
shadow = QGraphicsDropShadowEffect(self)
|
||||
shadow.setBlurRadius(18)
|
||||
shadow.setOffset(0, 3)
|
||||
shadow.setColor(QColor(0, 0, 0, 60))
|
||||
self.setGraphicsEffect(shadow)
|
||||
self.setFrameShape(QFrame.NoFrame)
|
||||
_style_card(self)
|
||||
lay = QVBoxLayout(self)
|
||||
self.title_lbl = QLabel("")
|
||||
self.title_lbl.setObjectName("hint")
|
||||
self.title_lbl.setStyleSheet("border: none;")
|
||||
self.value_lbl = QLabel("—")
|
||||
self.value_lbl.setStyleSheet("border: none; font-size: 20px; font-weight: 700;")
|
||||
self.value_lbl.setStyleSheet("border: none; font-size: 22px; font-weight: 600;")
|
||||
self.sub_lbl = QLabel("")
|
||||
self.sub_lbl.setObjectName("hint")
|
||||
self.sub_lbl.setStyleSheet("border: none;")
|
||||
@@ -66,20 +70,14 @@ class BudgetCard(QFrame):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setFrameShape(QFrame.StyledPanel)
|
||||
self.setStyleSheet(
|
||||
"QFrame { border: 1px solid rgba(140,146,152,0.35); border-radius: 16px; }")
|
||||
shadow = QGraphicsDropShadowEffect(self)
|
||||
shadow.setBlurRadius(18)
|
||||
shadow.setOffset(0, 3)
|
||||
shadow.setColor(QColor(0, 0, 0, 60))
|
||||
self.setGraphicsEffect(shadow)
|
||||
self.setFrameShape(QFrame.NoFrame)
|
||||
_style_card(self)
|
||||
lay = QVBoxLayout(self)
|
||||
self.title_lbl = QLabel("")
|
||||
self.title_lbl.setObjectName("hint")
|
||||
self.title_lbl.setStyleSheet("border: none;")
|
||||
self.value_lbl = QLabel("—")
|
||||
self._value_style = "border: none; font-size: 20px; font-weight: 700;"
|
||||
self._value_style = "border: none; font-size: 22px; font-weight: 600;"
|
||||
self.value_lbl.setStyleSheet(self._value_style)
|
||||
self.sub_lbl = QLabel("")
|
||||
self.sub_lbl.setObjectName("hint")
|
||||
@@ -109,7 +107,7 @@ class BudgetCard(QFrame):
|
||||
self.title_lbl.setText(title)
|
||||
self.value_lbl.setText(value)
|
||||
self.value_lbl.setStyleSheet(
|
||||
self._value_style + (" color: #E5484D;" if warn else ""))
|
||||
self._value_style + (f" color: {current_palette().danger};" if warn else ""))
|
||||
self.sub_lbl.setText(sub)
|
||||
|
||||
|
||||
@@ -185,15 +183,16 @@ class CollapseStrip(QWidget):
|
||||
p = QPainter(self)
|
||||
p.setRenderHint(QPainter.Antialiasing)
|
||||
w = self.width()
|
||||
accent = QColor(ACCENT) if self._hover else QColor("#8b8d98")
|
||||
tok = current_palette()
|
||||
accent = QColor(tok.accent) if self._hover else QColor(tok.text_faint)
|
||||
|
||||
# A small rounded "button" at the top carries the expand arrow so the
|
||||
# collapsed panel always shows a clear, clickable affordance.
|
||||
bw = min(w - 2.0, 16.0)
|
||||
btn = QRectF((w - bw) / 2.0, 6.0, bw, 18.0)
|
||||
p.setPen(QPen(QColor(139, 144, 150, 130), 1.0))
|
||||
p.setBrush(QColor(155, 160, 166, 70) if self._hover else QColor(155, 160, 166, 32))
|
||||
p.drawRoundedRect(btn, 4.0, 4.0)
|
||||
p.setPen(QPen(QColor(tok.border_strong), 1.0))
|
||||
p.setBrush(QColor(tok.hover if self._hover else tok.surface))
|
||||
p.drawRoundedRect(btn, float(tok.radius_sm), float(tok.radius_sm))
|
||||
|
||||
cx = w / 2.0
|
||||
cy = btn.center().y()
|
||||
@@ -211,7 +210,7 @@ class CollapseStrip(QWidget):
|
||||
|
||||
# thin handle line below the button
|
||||
p.setPen(Qt.NoPen)
|
||||
p.setBrush(QColor(155, 160, 166, 90))
|
||||
p.setBrush(QColor(tok.border_strong))
|
||||
line_w = 2.0
|
||||
x = (w - line_w) / 2.0
|
||||
ltop = btn.bottom() + 6.0
|
||||
@@ -226,7 +225,12 @@ class PlanSection(QWidget):
|
||||
close). Hidden until it has steps; updated in place as the agent calls
|
||||
``update_plan``."""
|
||||
|
||||
_COLORS = {STEP_RUNNING: ACCENT, STEP_DONE: "#6fe3a4", STEP_ERROR: "#ef6368"}
|
||||
@staticmethod
|
||||
def _step_color(status: str) -> str | None:
|
||||
"""Row text colour per step status; None leaves the default. Resolved
|
||||
per call so it follows a live theme switch."""
|
||||
p = current_palette()
|
||||
return {STEP_RUNNING: p.accent, STEP_DONE: p.success, STEP_ERROR: p.danger}.get(status)
|
||||
|
||||
@staticmethod
|
||||
def _step_icon(status: str):
|
||||
@@ -272,7 +276,7 @@ class PlanSection(QWidget):
|
||||
continue
|
||||
status = str((s or {}).get("status", STEP_PENDING)).strip().lower()
|
||||
item = QListWidgetItem(self._step_icon(status), f" {title}")
|
||||
color = self._COLORS.get(status)
|
||||
color = self._step_color(status)
|
||||
if color:
|
||||
item.setForeground(QColor(color))
|
||||
self.list.addItem(item)
|
||||
|
||||
Reference in New Issue
Block a user