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:
NamPDT
2026-08-17 11:38:18 +09:00
co-authored by Claude Opus 5
parent 414eaddca3
commit 291a611737
96 changed files with 12491 additions and 2937 deletions
+659 -295
View File
@@ -1,295 +1,659 @@
"""Qt style sheets giving the app a modern, dark design-tool look (deep
ocean-blue surfaces, large rounded corners, a deep-sea gradient accent, and
colorful pill badges) inspired by contemporary dashboard UIs."""
from __future__ import annotations
# Brand accent — deep sea blue palette with teal-cyan gradients.
ACCENT = "#0096C7"
ACCENT_HOVER = "#48CAE4"
ACCENT2 = "#0077B6" # deeper ocean blue for gradients
GRADIENT = f"qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 {ACCENT2}, stop:1 {ACCENT})"
GRADIENT_HOVER = f"qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #023E8A, stop:1 {ACCENT_HOVER})"
_DARK = f"""
* {{ font-family: "Segoe UI", "Helvetica Neue", "Arial", "Yu Gothic UI", "Meiryo", sans-serif; font-size: 13px; }}
QMainWindow, QWidget {{ background: #0A1628; color: #E0F0FF; }}
QMainWindow::separator {{ background: #0A1628; width: 4px; height: 4px; }}
QStatusBar {{ background: #0A1628; color: #5C8DB8; border-top: 1px solid #132240; }}
QSplitter::handle {{ background: #0A1628; }}
/* Separate the nav rail (its own panel + divider) from the content area. */
QWidget#navWrap {{ background: #0D1F35; border-right: 1px solid #17263f; }}
QWidget#navWrap QTreeWidget, QWidget#navWrap QListWidget {{ background: transparent; border: none; }}
QWidget#contentArea {{ background: #0A1628; }}
/* Compact icons app-wide (they looked oversized on scaled displays). */
QPushButton, QToolButton, QComboBox, QTabBar {{ qproperty-iconSize: 14px 14px; }}
QTreeWidget#navrail {{ qproperty-iconSize: 16px 16px; }}
/* Square the pane (tabs above stay rounded): a rounded pane lets its square
child pages poke past the corners — the "rectangle behind the rounded box". */
QTabWidget::pane {{ background: #0D1F35; border: 1px solid #132240; border-radius: 0px; top: 2px; }}
QTabBar {{ background: transparent; }}
QTabBar::tab {{
background: #111D32; color: #5C8DB8; padding: 9px 22px; margin: 0 6px 8px 0;
border-radius: 10px; border: 1px solid #132240; font-weight: 500;
}}
QTabBar::tab:selected {{ background: {GRADIENT}; color: white; border: none; font-weight: 700; }}
QTabBar::tab:hover:!selected {{ background: #172A45; color: #B0D4F1; }}
/* Co4E flow "browser" tabs: sit FLUSH in their row (no floating bottom gap, so
the icon/label is vertically centred) and use the app's panel/accent surfaces
so the strip reads as part of the app. */
QTabBar#flowTabs::tab {{ background: #0D1F35; color: #8FB2D4; border: 1px solid #17263f;
padding: 6px 10px; margin: 0 3px 0 0; min-height: 22px; border-radius: 8px; }}
QTabBar#flowTabs::tab:selected {{ background: {GRADIENT}; color: white; border: 1px solid transparent; }}
QTabBar#flowTabs::tab:hover:!selected {{ background: #172A45; color: #E0F0FF; }}
/* The "new flow" (+) button — styled as the last tab in the strip. */
QPushButton#flowAddBtn {{ background: #0D1F35; color: #8FB2D4; border: 1px solid #17263f;
border-radius: 8px; padding: 6px 0; font-size: 15px; font-weight: bold; min-height: 22px; }}
QPushButton#flowAddBtn:hover {{ background: #172A45; color: #E0F0FF; }}
/* Co4E sidebar (Workflows/Agents/Skills): transparent icon tabs with a subtle
translucent selection + the normal text colour (matches the app's lists,
not a bright fill). */
QTabBar#co4eSideTabs {{ qproperty-iconSize: 18px 18px; }}
QTabBar#co4eSideTabs::tab {{ background: transparent; color: #8FB2D4; border: none;
border-radius: 6px; padding: 5px; margin: 0 6px 0 0; }}
QTabBar#co4eSideTabs::tab:selected {{ background: rgba(0,150,199,0.28); color: #E0F0FF; }}
QTabBar#co4eSideTabs::tab:hover:!selected {{ background: #172A45; color: #B0D4F1; }}
/* Co4E canvas frame — match the app's other framed surfaces (not a faint hairline). */
QGraphicsView#co4eCanvas {{ background: #0D1F35; border: 1px solid #1A2D4A; border-radius: 10px; }}
QGroupBox {{
background: #0D1F35; border: 1px solid #132240; border-radius: 18px;
margin-top: 16px; padding: 14px 10px 10px 10px; font-weight: 700;
}}
QGroupBox::title {{
subcontrol-origin: margin; subcontrol-position: top left; left: 12px; top: 2px;
padding: 0 6px; color: #5C8DB8; letter-spacing: 0.5px;
}}
QPlainTextEdit, QTextEdit, QTextBrowser, QLineEdit, QTreeView, QListView, QTreeWidget, QListWidget {{
background: #111D32; color: #E0F0FF; border: 1px solid #1A2D4A; border-radius: 10px;
selection-background-color: {ACCENT}; selection-color: white;
}}
/* Scroll containers must NOT paint their own square panel behind rounded
children (that square is what shows as a "rectangle under the rounded box").
The corner where scrollbars meet is squared off too — keep it transparent. */
QScrollArea {{ background: transparent; border: none; }}
QAbstractScrollArea::corner {{ background: transparent; }}
QPlainTextEdit:focus, QTextEdit:focus, QLineEdit:focus {{ border: 1px solid {ACCENT}; }}
QTreeView::item, QListView::item {{ padding: 3px 2px; border-radius: 6px; }}
QTreeView::item:hover, QListView::item:hover {{ background: #172A45; }}
QTreeView::item:selected, QListView::item:selected {{ background: rgba(0,150,199,0.28); color: #E0F0FF; }}
QHeaderView::section {{ background: #111D32; color: #5C8DB8; border: none; border-bottom: 1px solid #132240; padding: 6px; }}
QPushButton {{
background: #132240; color: #E0F0FF; border: 1px solid #1A2D4A;
border-radius: 10px; padding: 8px 16px;
}}
QPushButton:hover {{ background: #1A3050; border-color: #234070; }}
QPushButton:pressed {{ background: #111D32; }}
QPushButton:disabled {{ color: #3A5A78; background: #0F1A28; border-color: #132240; }}
QPushButton#primary {{ background: {GRADIENT}; color: white; border: none; font-weight: 700; padding: 8px 18px; }}
QPushButton#primary:hover {{ background: {GRADIENT_HOVER}; }}
QPushButton#primary:disabled {{ background: #1A2D4A; color: #3A5A78; }}
QPushButton#danger {{ background: #E5484D; color: white; border: none; font-weight: 600; }}
QPushButton#danger:hover {{ background: #EF6368; }}
QPushButton#navMenuBtn {{
background: transparent; border: none; border-radius: 8px; padding: 5px 6px;
font-weight: 700; font-size: 11px; letter-spacing: 1px; color: #5C8DB8; text-align: left;
}}
QPushButton#navMenuBtn:hover {{ background: #132240; color: #E0F0FF; }}
QPushButton#navMenuBtn:pressed {{ background: #111D32; }}
QLabel#badge {{ background: #0A2A3A; color: #48CAE4; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#badgeSuccess {{ background: #0A2A20; color: #48D9A0; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#badgePurple {{ background: #1A1A3A; color: #9B8FF7; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#badgePink {{ background: #2A1A30; color: #D980C0; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#badgeWarn {{ background: #0A2A3A; color: #48CAE4; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#hint {{ color: #5C8DB8; }}
QLabel#warning {{ color: #48CAE4; font-weight: 600; }}
QComboBox {{ background: #111D32; border: 1px solid #1A2D4A; border-radius: 10px; padding: 6px 10px; }}
QComboBox:hover {{ border-color: #234070; }}
QComboBox::drop-down {{ border: none; width: 22px; }}
QComboBox QAbstractItemView {{
background: #111D32; border: 1px solid #1A2D4A; border-radius: 10px;
selection-background-color: {ACCENT}; selection-color: white; outline: none;
}}
QScrollBar:vertical {{ background: transparent; width: 11px; margin: 2px; }}
QScrollBar::handle:vertical {{ background: #1A2D4A; border-radius: 5px; min-height: 28px; }}
QScrollBar::handle:vertical:hover {{ background: {ACCENT}; }}
QScrollBar:horizontal {{ background: transparent; height: 11px; margin: 2px; }}
QScrollBar::handle:horizontal {{ background: #1A2D4A; border-radius: 5px; min-width: 28px; }}
QScrollBar::handle:horizontal:hover {{ background: {ACCENT}; }}
QScrollBar::add-line, QScrollBar::sub-line {{ height: 0; width: 0; border: none; background: none; }}
QCheckBox {{ spacing: 8px; }}
QCheckBox::indicator, QRadioButton::indicator {{
width: 16px; height: 16px; background: #111D32; border: 1px solid #1A2D4A; border-radius: 5px;
}}
QRadioButton::indicator {{ border-radius: 9px; }}
QCheckBox::indicator:hover, QRadioButton::indicator:hover {{ border-color: {ACCENT}; }}
QCheckBox::indicator:checked, QRadioButton::indicator:checked {{
background: {GRADIENT}; border-color: {ACCENT};
}}
QCheckBox::indicator:disabled {{ border-color: #132240; background: #0F1A28; }}
QCheckBox::indicator:checked:disabled, QRadioButton::indicator:checked:disabled {{
background: #1A3050; border-color: #234070;
}}
QMenu {{ background: #111D32; color: #E0F0FF; border: 1px solid #1A2D4A; border-radius: 10px; padding: 4px; }}
QMenu::item {{ padding: 6px 16px; border-radius: 6px; }}
QMenu::item:selected {{ background: {ACCENT}; color: white; }}
QToolTip {{ background: #172A45; color: #E0F0FF; border: 1px solid #1A2D4A; border-radius: 6px; padding: 4px 8px; }}
"""
_LIGHT = f"""
* {{ font-family: "Segoe UI", "Helvetica Neue", "Arial", "Yu Gothic UI", "Meiryo", sans-serif; font-size: 13px; }}
QMainWindow, QWidget {{ background: #E8F4FD; color: #1A2332; }}
QStatusBar {{ background: #E8F4FD; color: #5C8DB8; border-top: 1px solid #B8D4E8; }}
/* Separate the nav rail (its own panel + divider) from the content area. */
QWidget#navWrap {{ background: #EDF5FB; border-right: 1px solid #C4DBEC; }}
QWidget#navWrap QTreeWidget, QWidget#navWrap QListWidget {{ background: transparent; border: none; }}
QWidget#contentArea {{ background: #E8F4FD; }}
/* Compact icons app-wide (they looked oversized on scaled displays). */
QPushButton, QToolButton, QComboBox, QTabBar {{ qproperty-iconSize: 14px 14px; }}
QTreeWidget#navrail {{ qproperty-iconSize: 16px 16px; }}
QTabWidget::pane {{ background: #FFFFFF; border: 1px solid #B8D4E8; border-radius: 0px; top: 2px; }}
QTabBar {{ background: transparent; }}
QTabBar::tab {{
background: #D0E8F5; color: #3A6B8C; padding: 9px 22px; margin: 0 6px 8px 0;
border-radius: 10px; border: 1px solid #B8D4E8; font-weight: 500;
}}
QTabBar::tab:selected {{ background: {GRADIENT}; color: white; border: none; font-weight: 700; }}
QTabBar::tab:hover:!selected {{ background: #B8D4E8; color: #1A2332; }}
/* Co4E flow "browser" tabs — light-theme counterpart (see dark block). */
QTabBar#flowTabs::tab {{ background: #EDF5FB; color: #3A6B8C; border: 1px solid #C4DBEC;
padding: 6px 10px; margin: 0 3px 0 0; min-height: 22px; border-radius: 8px; }}
QTabBar#flowTabs::tab:selected {{ background: {GRADIENT}; color: white; border: 1px solid transparent; }}
QTabBar#flowTabs::tab:hover:!selected {{ background: #D0E8F5; color: #1A2332; }}
/* The "new flow" (+) button (light) — styled as the last tab in the strip. */
QPushButton#flowAddBtn {{ background: #EDF5FB; color: #3A6B8C; border: 1px solid #C4DBEC;
border-radius: 8px; padding: 6px 0; font-size: 15px; font-weight: bold; min-height: 22px; }}
QPushButton#flowAddBtn:hover {{ background: #D0E8F5; color: #1A2332; }}
/* Co4E sidebar (light) — transparent tabs, translucent selection, dark text. */
QTabBar#co4eSideTabs {{ qproperty-iconSize: 18px 18px; }}
QTabBar#co4eSideTabs::tab {{ background: transparent; color: #3A6B8C; border: none;
border-radius: 6px; padding: 5px; margin: 0 6px 0 0; }}
QTabBar#co4eSideTabs::tab:selected {{ background: rgba(0,150,199,0.20); color: #1A2332; }}
QTabBar#co4eSideTabs::tab:hover:!selected {{ background: #D0E8F5; color: #1A2332; }}
/* Co4E canvas frame (light). */
QGraphicsView#co4eCanvas {{ background: #FFFFFF; border: 1px solid #B8D4E8; border-radius: 10px; }}
QGroupBox {{
background: #FFFFFF; border: 1px solid #B8D4E8; border-radius: 18px;
margin-top: 16px; padding: 14px 10px 10px 10px; font-weight: 700;
}}
QGroupBox::title {{
subcontrol-origin: margin; subcontrol-position: top left; left: 12px; top: 2px;
padding: 0 6px; color: #5C8DB8; letter-spacing: 0.5px;
}}
QPlainTextEdit, QTextEdit, QTextBrowser, QLineEdit, QTreeView, QListView, QTreeWidget, QListWidget {{
background: white; color: #1A2332; border: 1px solid #C0D8EC; border-radius: 10px;
selection-background-color: {ACCENT}; selection-color: white;
}}
QScrollArea {{ background: transparent; border: none; }}
QAbstractScrollArea::corner {{ background: transparent; }}
QPlainTextEdit:focus, QTextEdit:focus, QLineEdit:focus {{ border: 1px solid {ACCENT}; }}
QTreeView::item, QListView::item {{ padding: 3px 2px; border-radius: 6px; }}
QTreeView::item:hover, QListView::item:hover {{ background: #E0EFFA; }}
QTreeView::item:selected, QListView::item:selected {{ background: rgba(0,150,199,0.18); color: #1A2332; }}
QHeaderView::section {{ background: #E8F4FD; color: #5C8DB8; border: none; border-bottom: 1px solid #B8D4E8; padding: 6px; }}
QPushButton {{
background: white; color: #1A2332; border: 1px solid #C0D8EC;
border-radius: 10px; padding: 8px 16px;
}}
QPushButton:hover {{ background: #E0EFFA; }}
QPushButton:disabled {{ color: #8AA8C0; background: #F0F8FC; }}
QPushButton#primary {{ background: {GRADIENT}; color: white; border: none; font-weight: 700; padding: 8px 18px; }}
QPushButton#primary:hover {{ background: {GRADIENT_HOVER}; }}
QPushButton#danger {{ background: #E5484D; color: white; border: none; font-weight: 600; }}
QPushButton#danger:hover {{ background: #EF6368; }}
QPushButton#navMenuBtn {{
background: transparent; border: none; border-radius: 8px; padding: 5px 6px;
font-weight: 700; font-size: 11px; letter-spacing: 1px; color: #5C8DB8; text-align: left;
}}
QPushButton#navMenuBtn:hover {{ background: #D0E8F5; color: #1A2332; }}
QPushButton#navMenuBtn:pressed {{ background: #B8D4E8; }}
QLabel#badge {{ background: #D0ECF8; color: #0077B6; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#badgeSuccess {{ background: #D0F5E8; color: #1B7A3D; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#badgePurple {{ background: #E8E0FF; color: #6238C9; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#badgePink {{ background: #F8E0F0; color: #B93A85; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#badgeWarn {{ background: #D0ECF8; color: #0077B6; border-radius: 11px; padding: 2px 12px; font-weight: 600; }}
QLabel#hint {{ color: #5C8DB8; }}
QLabel#warning {{ color: #0077B6; font-weight: 600; }}
QComboBox {{ background: white; border: 1px solid #C0D8EC; border-radius: 10px; padding: 6px 10px; }}
QComboBox::drop-down {{ border: none; width: 22px; }}
QComboBox QAbstractItemView {{
background: white; border: 1px solid #C0D8EC; border-radius: 10px;
selection-background-color: {ACCENT}; selection-color: white; outline: none;
}}
QScrollBar:vertical {{ background: transparent; width: 11px; margin: 2px; }}
QScrollBar::handle:vertical {{ background: #C0D8EC; border-radius: 5px; min-height: 28px; }}
QScrollBar::handle:vertical:hover {{ background: {ACCENT}; }}
QScrollBar:horizontal {{ background: transparent; height: 11px; margin: 2px; }}
QScrollBar::handle:horizontal {{ background: #C0D8EC; border-radius: 5px; min-width: 28px; }}
QScrollBar::handle:horizontal:hover {{ background: {ACCENT}; }}
QScrollBar::add-line, QScrollBar::sub-line {{ height: 0; width: 0; border: none; background: none; }}
QCheckBox {{ spacing: 8px; }}
QCheckBox::indicator, QRadioButton::indicator {{
width: 16px; height: 16px; background: white; border: 1px solid #C0D8EC; border-radius: 5px;
}}
QRadioButton::indicator {{ border-radius: 9px; }}
QCheckBox::indicator:hover, QRadioButton::indicator:hover {{ border-color: {ACCENT}; }}
QCheckBox::indicator:checked, QRadioButton::indicator:checked {{
background: {GRADIENT}; border-color: {ACCENT};
}}
QCheckBox::indicator:disabled {{ border-color: #D0E8F5; background: #F0F8FC; }}
QCheckBox::indicator:checked:disabled, QRadioButton::indicator:checked:disabled {{
background: #B8D4E8; border-color: #8AB8D8;
}}
QMenu {{ background: white; color: #1A2332; border: 1px solid #C0D8EC; border-radius: 10px; padding: 4px; }}
QMenu::item {{ padding: 6px 16px; border-radius: 6px; }}
QMenu::item:selected {{ background: {ACCENT}; color: white; }}
QToolTip {{ background: #FFFFFF; color: #1A2332; border: 1px solid #C0D8EC; border-radius: 6px; padding: 4px 8px; }}
"""
# Node colors used by the Graph view (shared light/dark).
NODE_COLORS = {
"user": "#3B82F6",
"assistant": ACCENT,
"tool": "#8B5CF6",
"result": "#22A06B",
"error": "#E5484D",
}
def resolve_theme(theme: str) -> str:
"""Resolve 'system' to 'dark'/'light' based on the OS color scheme."""
if theme != "system":
return theme
try:
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication
app = QApplication.instance()
if app is not None:
scheme = app.styleHints().colorScheme()
return "light" if scheme == Qt.ColorScheme.Light else "dark"
except Exception:
pass
return "dark"
def stylesheet(theme: str) -> str:
return _LIGHT if resolve_theme(theme) == "light" else _DARK
"""The app's visual system: semantic design tokens + one style sheet template.
This replaces the previous mechanism, which was two hand-written Qt style
sheets (``_DARK`` / ``_LIGHT``) that duplicated each other and hard-coded ~105
hex literals, with a single source of truth:
Palette (tokens) -> _TEMPLATE (one QSS) -> stylesheet(theme)
Rules of the system
-------------------
* **Nothing outside this module names a colour.** Widgets that paint with
``QPainter`` (charts, canvases, syntax highlighters) call :func:`palette` and
read a token. Widgets that style themselves declaratively should instead be
given an ``objectName`` and styled in ``_TEMPLATE`` below.
* **Tokens are semantic, not literal.** ``danger``/``text_muted``/``code_string``
— never ``blue``/``grey2``. Adding a theme means adding a :class:`Palette`,
not editing a style sheet.
* **No gradients, no glows.** The palette is Visual Studio Code's — "Dark
Modern" and "Light Modern", taken from the shipped theme JSON. Flat surfaces,
square-ish corners, one accent spent only on what the user acts on. Depth
comes from the surface ramp and hairline borders, not from colour. Note the
VS Code silhouette: the nav rail is *darker* than the content area, not
lighter.
Contrast is held to WCAG AA (4.5:1) for body text and for text on filled
buttons. That is why ``accent`` and ``accent_solid`` are separate tokens: on a
dark background a blue readable *as text* is too light to carry white *as a
fill*, so each role gets the tint that passes.
Four VS Code values fall below AA and are nudged just far enough to clear it —
dark line numbers (3.59:1), light faint text on the sidebar (4.28:1), light
green (4.33:1) and light amber (3.12:1). Each carries a comment naming the
original value, so the deviation is auditable rather than silent.
"""
from __future__ import annotations
from dataclasses import dataclass, asdict
from string import Template
@dataclass(frozen=True)
class Palette:
"""Every colour and shape value the interface is allowed to use."""
name: str
# --- surfaces: a 4-step ramp from the window back to the frontmost layer.
bg: str # window / canvas backdrop
surface: str # panels, cards, group boxes (NOT the nav rail)
surface_raised: str # inputs, lists, trees — things you type or pick in
overlay: str # menus, tooltips, popups (floats above everything)
sunken: str # logs, code, terminals — things you read into
hover: str # hover wash on rows, tabs, ghost buttons
active: str # pressed / held state
# The nav rail gets its own step rather than borrowing `surface`. It is a
# permanent region of the window, not a card floating on the page.
#
# Following VS Code, the rail is *darker* than the content area (dark) or a
# shade off white (light). The step is small on purpose — VS Code separates
# the rail with a border, not a big tonal jump — so `nav_border` is doing
# real work here and must stay visible.
nav_bg: str
nav_border: str
nav_hover: str
nav_selected: str
# --- lines
border: str # default hairline
border_strong: str # hairline that must survive next to a filled surface
focus_ring: str # keyboard/typing focus
# --- text
text: str
text_muted: str # secondary copy, captions, group-box titles
text_faint: str # metadata, timestamps, placeholder
text_disabled: str
on_accent: str # text drawn on top of a filled accent/status surface
# --- accent: `accent` tints text & icons, `accent_solid` fills buttons.
accent: str
accent_solid: str
accent_solid_hover: str
accent_solid_active: str
accent_soft: str # translucent wash for selected rows (QSS only)
accent_soft_hover: str
accent_wash: str # the same tint pre-blended to a solid, for Qt rich
# text (bgcolor=, <table>) where alpha is ignored
# --- status
success: str
success_soft: str
warning: str
warning_soft: str
danger: str
danger_solid: str
danger_solid_hover: str
danger_soft: str
info: str
info_soft: str
purple: str
purple_soft: str
pink: str
pink_soft: str
# --- selection (text selection inside editors and inputs)
selection_bg: str
selection_fg: str
# --- scrollbars
scroll_handle: str
scroll_handle_hover: str
# --- code & terminal
code_bg: str
code_fg: str
code_gutter_bg: str
code_gutter_fg: str
code_selection: str
code_comment: str
code_keyword: str
code_type: str
code_func: str
code_attr: str
code_string: str
code_number: str
code_error: str
# --- diff / inline change badges
diff_add_bg: str
diff_add_fg: str
diff_del_bg: str
diff_del_fg: str
# --- charts
chart_grid: str
chart_label: str
# --- conversation & graph node roles
role_user: str
role_assistant: str
role_tool: str
role_result: str
role_error: str
# --- shape & type
radius_sm: int
radius: int
radius_lg: int
font_family: str
font_size: int
font_mono: str
_FONT = '"Segoe UI Variable Text", "Segoe UI", "Inter", "Helvetica Neue", "Yu Gothic UI", "Meiryo", sans-serif'
_MONO = '"Cascadia Code", "JetBrains Mono", "Consolas", "SF Mono", monospace'
DARK = Palette(
name="dark",
# ---- VS Code "Dark Modern" ----------------------------------------------
# Values taken from the shipped theme JSON. Where VS Code's own choice falls
# below WCAG AA it is nudged just far enough to pass; each such value carries
# a note with VS Code's original and the measured ratio.
bg="#1F1F1F", # editor.background
surface="#252526", # panel / card
surface_raised="#313131", # input.background
overlay="#252526", # menus, tooltips
sunken="#181818", # logs, terminals — below the ramp
hover="#2A2D2E", # list.hoverBackground
active="#37373D", # list.inactiveSelectionBackground
# The sidebar is DARKER than the editor — that is the VS Code silhouette.
nav_bg="#181818", # sideBar.background
nav_border="#2B2B2B", # sideBar.border
nav_hover="#2A2D2E",
nav_selected="#04395E", # list.activeSelectionBackground
border="#2B2B2B", # panel.border
border_strong="#3C3C3C", # input.border
focus_ring="#0078D4", # focusBorder
text="#CCCCCC", # editor.foreground
text_muted="#9D9D9D", # descriptionForeground
text_faint="#9A9A9A", # lifted: #8B8B8B was 3.82:1 on input surfaces
text_disabled="#5A5A5A",
on_accent="#FFFFFF",
accent="#4DAAFC", # textLink.foreground — accent as TEXT
accent_solid="#0078D4", # button.background — accent as FILL
accent_solid_hover="#026EC1",
accent_solid_active="#005FB8",
accent_soft="rgba(0,120,212,0.22)",
accent_soft_hover="rgba(0,120,212,0.32)",
accent_wash="#12283C", # pre-blended: Qt rich text ignores alpha
success="#89D185", # gitDecoration added
success_soft="rgba(137,209,133,0.16)",
warning="#CCA700", # editorWarning
warning_soft="rgba(204,167,0,0.16)",
danger="#F76464", # editorError #F14C4C lifted (4.29:1 on panels)
danger_solid="#C4302B",
danger_solid_hover="#D9433C",
danger_soft="rgba(241,76,76,0.16)",
info="#4DAAFC",
info_soft="rgba(77,170,252,0.16)",
purple="#C586C0", # Dark+ syntax purple
purple_soft="rgba(197,134,192,0.16)",
pink="#D16D9E",
pink_soft="rgba(209,109,158,0.16)",
selection_bg="#264F78", # editor.selectionBackground
selection_fg="#FFFFFF",
scroll_handle="#4E4E4E", # scrollbarSlider
scroll_handle_hover="#5A5A5A",
code_bg="#1F1F1F",
code_fg="#CCCCCC",
code_gutter_bg="#1F1F1F",
# VS Code uses #6E7681 for line numbers — only 3.59:1. Lifted to clear AA.
code_gutter_fg="#858D97",
code_selection="#264F78",
code_comment="#6A9955", # ---- Dark+ syntax, unchanged --------------
code_keyword="#569CD6",
code_type="#4EC9B0",
code_func="#DCDCAA",
code_attr="#9CDCFE",
code_string="#CE9178",
code_number="#B5CEA8",
code_error="#F44747",
diff_add_bg="#1B3A1B", # diffEditor inserted, pre-blended
diff_add_fg="#89D185",
diff_del_bg="#4B1818", # diffEditor removed, pre-blended
diff_del_fg="#F76464",
chart_grid="#2B2B2B",
chart_label="#9D9D9D",
role_user="#4DAAFC",
role_assistant="#4EC9B0",
role_tool="#C586C0",
role_result="#89D185",
role_error="#F14C4C",
radius_sm=3, # VS Code is squarer than the previous look
radius=4,
radius_lg=6,
font_family=_FONT,
font_size=13,
font_mono=_MONO,
)
LIGHT = Palette(
name="light",
# ---- VS Code "Light Modern" ---------------------------------------------
bg="#FFFFFF", # editor.background
surface="#F8F8F8", # sideBar / panel
surface_raised="#FFFFFF", # input.background
overlay="#FFFFFF",
sunken="#F3F3F3",
hover="#F2F2F2", # list.hoverBackground
active="#E8E8E8", # list.activeSelectionBackground
nav_bg="#F8F8F8", # sideBar.background
nav_border="#E5E5E5", # sideBar.border
nav_hover="#F2F2F2",
nav_selected="#E4E6F1", # active row, tinted toward the accent
border="#E5E5E5",
border_strong="#CECECE", # input.border
focus_ring="#005FB8", # focusBorder
text="#3B3B3B", # editor.foreground
text_muted="#616161",
# VS Code uses #767676 — 4.28:1 on the sidebar. Darkened to clear AA there.
text_faint="#6E6E6E",
text_disabled="#A0A0A0",
on_accent="#FFFFFF",
accent="#005FB8", # textLink / button
accent_solid="#005FB8",
accent_solid_hover="#0258A8",
accent_solid_active="#004C97",
accent_soft="rgba(0,95,184,0.10)",
accent_soft_hover="rgba(0,95,184,0.16)",
accent_wash="#E6EEF8",
# VS Code green #388A34 is 4.33:1 and amber #BF8803 only 3.12:1 — both lifted.
success="#317A2D",
success_soft="#DFF3DE",
warning="#8F6500", # VS Code #BF8803 = 3.12:1
warning_soft="#FBF0D0",
danger="#CD3131", # editorError
danger_solid="#CD3131",
danger_solid_hover="#B82A2A",
danger_soft="#FDEFEF", # lightened so #CD3131 clears AA on it
info="#005FB8",
info_soft="#DDEBF9",
purple="#6F42C1",
purple_soft="#EDE7FA",
pink="#B3247E",
pink_soft="#FAE3F0",
selection_bg="#ADD6FF", # editor.selectionBackground
selection_fg="#000000",
scroll_handle="#C1C1C1",
scroll_handle_hover="#A6A6A6",
code_bg="#FFFFFF",
code_fg="#3B3B3B",
code_gutter_bg="#F8F8F8",
code_gutter_fg="#656C76", # VS Code #6E7681 = 4.33:1 on the gutter
code_selection="#ADD6FF",
code_comment="#008000", # ---- Light+ syntax ------------------------
code_keyword="#0000FF",
code_type="#267F99",
code_func="#795E26",
code_attr="#E50000",
code_string="#A31515",
code_number="#098658",
code_error="#CD3131",
diff_add_bg="#DBF4DB",
diff_add_fg="#1E6F1A",
diff_del_bg="#FBE3E3",
diff_del_fg="#B82A2A",
chart_grid="#E5E5E5",
chart_label="#616161",
role_user="#005FB8",
role_assistant="#267F99",
role_tool="#6F42C1",
role_result="#317A2D",
role_error="#CD3131",
radius_sm=3,
radius=4,
radius_lg=6,
font_family=_FONT,
font_size=13,
font_mono=_MONO,
)
_PALETTES = {"dark": DARK, "light": LIGHT}
# ---------------------------------------------------------------------------
# The one style sheet. `$token` placeholders are filled from the Palette above;
# use `${token}px` where a unit follows the name.
#
# Read it as a cascade: reset -> shell -> surfaces -> controls -> chrome.
# ---------------------------------------------------------------------------
_TEMPLATE = Template("""
/* ---- reset ------------------------------------------------------------ */
* { font-family: $font_family; font-size: ${font_size}px; }
QWidget { background: $bg; color: $text; }
QMainWindow::separator { background: $border; width: 1px; height: 1px; }
QSplitter::handle { background: $border; }
QSplitter::handle:horizontal { width: 1px; }
QSplitter::handle:vertical { height: 1px; }
QSplitter::handle:hover { background: $border_strong; }
QStatusBar { background: $bg; color: $text_faint; border-top: 1px solid $border; }
QToolTip {
background: $overlay; color: $text; border: 1px solid $border_strong;
border-radius: ${radius}px; padding: 5px 9px;
}
/* Icons are drawn at text scale, not as decoration. */
QPushButton, QToolButton, QComboBox, QTabBar { qproperty-iconSize: 15px 15px; }
QTreeWidget#navrail { qproperty-iconSize: 16px 16px; }
/* ---- shell ------------------------------------------------------------ */
QWidget#topbar { background: $bg; border: none; border-bottom: 1px solid $border; }
QLabel#brand { color: $text; font-size: 15px; font-weight: 700; background: transparent; }
QWidget#navWrap { background: $nav_bg; border-right: 1px solid $nav_border; }
QWidget#navWrap QTreeWidget, QWidget#navWrap QListWidget { background: transparent; border: none; }
QWidget#contentArea { background: $bg; }
/* Rail rows sit on nav_bg, so they need their own hover/selected steps — the
generic ones are tuned against `bg` and wash out here. The active item also
carries a 2px accent marker, so which section you are in survives even at a
glance or for anyone who cannot separate the two greys. */
QWidget#navWrap QTreeWidget::item, QWidget#navWrap QListWidget::item {
padding: 6px 4px; border-radius: ${radius}px;
}
QWidget#navWrap QTreeWidget::item:hover, QWidget#navWrap QListWidget::item:hover {
background: $nav_hover;
}
QWidget#navWrap QTreeWidget::item:selected, QWidget#navWrap QListWidget::item:selected {
background: $nav_selected; color: $text;
border-left: 2px solid $accent; font-weight: 600;
}
/* ---- surfaces --------------------------------------------------------- */
QGroupBox {
background: $surface; border: 1px solid $border; border-radius: ${radius_lg}px;
margin-top: 14px; padding: 16px 12px 12px 12px; font-weight: 600;
}
QGroupBox::title {
subcontrol-origin: margin; subcontrol-position: top left; left: 12px; top: 1px;
padding: 0 6px; color: $text_muted; font-size: 12px; font-weight: 600;
}
QFrame[frameShape="4"], QFrame[frameShape="5"] { background: $border; border: none; }
QScrollArea { background: transparent; border: none; }
QAbstractScrollArea::corner { background: transparent; }
/* ---- tabs: an underline, not a pill. -------------------------------------
The old pill tabs read as buttons and fought the real buttons for
attention. A 2px rule under the active label is quieter and unambiguous. */
QTabWidget::pane { background: $bg; border: none; border-top: 1px solid $border; top: -1px; }
QTabBar { background: transparent; qproperty-drawBase: 0; }
QTabBar::tab {
background: transparent; color: $text_muted; padding: 9px 14px; margin: 0 2px 0 0;
border: none; border-bottom: 2px solid transparent; font-weight: 500;
}
QTabBar::tab:selected { color: $text; border-bottom: 2px solid $accent; font-weight: 600; }
QTabBar::tab:hover:!selected { color: $text; background: $hover; }
/* Co4E flow strip — browser-style tabs, so these stay enclosed. */
QTabBar#flowTabs::tab {
background: $surface; color: $text_muted; border: 1px solid $border;
border-radius: ${radius}px; padding: 6px 10px; margin: 0 3px 0 0; min-height: 22px;
}
QTabBar#flowTabs::tab:selected { background: $surface_raised; color: $text; border-color: $border_strong; }
QTabBar#flowTabs::tab:hover:!selected { background: $hover; color: $text; }
QPushButton#flowAddBtn {
background: transparent; color: $text_muted; border: 1px solid $border;
border-radius: ${radius}px; padding: 6px 0; font-size: 15px; min-height: 22px;
}
QPushButton#flowAddBtn:hover { background: $hover; color: $text; }
/* Co4E icon sidebar — no chrome until it is the active one. */
QTabBar#co4eSideTabs { qproperty-iconSize: 18px 18px; }
QTabBar#co4eSideTabs::tab {
background: transparent; color: $text_muted; border: none;
border-radius: ${radius}px; padding: 6px; margin: 0 4px 0 0;
}
QTabBar#co4eSideTabs::tab:selected { background: $accent_soft; color: $text; }
QTabBar#co4eSideTabs::tab:hover:!selected { background: $hover; color: $text; }
QGraphicsView#co4eCanvas {
background: $surface; border: 1px solid $border; border-radius: ${radius_lg}px;
}
/* ---- text entry & item views ------------------------------------------ */
QPlainTextEdit, QTextEdit, QTextBrowser, QLineEdit, QSpinBox, QDoubleSpinBox,
QTreeView, QListView, QTableView, QTreeWidget, QListWidget, QTableWidget {
background: $surface_raised; color: $text; border: 1px solid $border;
border-radius: ${radius}px; selection-background-color: $selection_bg;
selection-color: $selection_fg;
}
QPlainTextEdit, QTextEdit, QLineEdit, QSpinBox, QDoubleSpinBox { padding: 6px 8px; }
QPlainTextEdit:hover, QTextEdit:hover, QLineEdit:hover { border-color: $border_strong; }
QPlainTextEdit:focus, QTextEdit:focus, QLineEdit:focus,
QSpinBox:focus, QDoubleSpinBox:focus { border: 1px solid $focus_ring; }
QLineEdit:disabled, QPlainTextEdit:disabled, QTextEdit:disabled {
background: $surface; color: $text_disabled;
}
QTreeView::item, QListView::item, QTableView::item { padding: 4px 3px; border-radius: ${radius_sm}px; }
QTreeView::item:hover, QListView::item:hover { background: $hover; }
QTreeView::item:selected, QListView::item:selected, QTableView::item:selected {
background: $accent_soft; color: $text;
}
QHeaderView::section {
background: $bg; color: $text_muted; border: none;
border-bottom: 1px solid $border; padding: 7px 6px; font-weight: 600;
}
/* ---- buttons -----------------------------------------------------------
Default is a quiet outline. Weight is reserved for #primary / #danger, so
at most one button per view should carry a fill. */
QPushButton {
background: $surface_raised; color: $text; border: 1px solid $border_strong;
border-radius: ${radius}px; padding: 7px 14px; font-weight: 500;
}
QPushButton:hover { background: $hover; border-color: $border_strong; }
QPushButton:pressed { background: $active; }
QPushButton:disabled { color: $text_disabled; background: $surface; border-color: $border; }
QPushButton:focus { border: 1px solid $focus_ring; }
QPushButton#primary {
background: $accent_solid; color: $on_accent; border: 1px solid transparent; font-weight: 600;
}
QPushButton#primary:hover { background: $accent_solid_hover; }
QPushButton#primary:pressed { background: $accent_solid_active; }
QPushButton#primary:disabled { background: $surface; color: $text_disabled; border-color: $border; }
QPushButton#danger {
background: $danger_solid; color: $on_accent; border: 1px solid transparent; font-weight: 600;
}
QPushButton#danger:hover { background: $danger_solid_hover; }
QPushButton#danger:disabled { background: $surface; color: $text_disabled; border-color: $border; }
/* Ghost buttons: nav section headers and icon-only chrome. */
QPushButton#navMenuBtn {
background: transparent; border: none; border-radius: ${radius}px; padding: 5px 6px;
font-weight: 600; font-size: 11px; letter-spacing: 0.6px; color: $text_faint; text-align: left;
}
QPushButton#navMenuBtn:hover { background: $hover; color: $text; }
QPushButton#navMenuBtn:pressed { background: $active; }
QToolButton {
background: transparent; color: $text_muted; border: none;
border-radius: ${radius}px; padding: 5px;
}
QToolButton:hover { background: $hover; color: $text; }
QToolButton:pressed { background: $active; }
QToolButton::menu-indicator { image: none; }
/* ---- pickers ----------------------------------------------------------- */
QComboBox {
background: $surface_raised; color: $text; border: 1px solid $border_strong;
border-radius: ${radius}px; padding: 6px 10px;
}
QComboBox:hover { background: $hover; }
QComboBox:focus { border-color: $focus_ring; }
QComboBox:disabled { color: $text_disabled; background: $surface; border-color: $border; }
QComboBox::drop-down { border: none; width: 20px; }
QComboBox QAbstractItemView {
background: $overlay; color: $text; border: 1px solid $border_strong;
border-radius: ${radius}px; padding: 4px; outline: none;
selection-background-color: $accent_soft; selection-color: $text;
}
QMenu { background: $overlay; color: $text; border: 1px solid $border_strong;
border-radius: ${radius}px; padding: 4px; }
QMenu::item { padding: 6px 14px; border-radius: ${radius_sm}px; }
QMenu::item:selected { background: $accent_soft; color: $text; }
QMenu::item:disabled { color: $text_disabled; }
QMenu::separator { height: 1px; background: $border; margin: 4px 6px; }
QMenuBar { background: $bg; color: $text; border-bottom: 1px solid $border; }
QMenuBar::item { padding: 5px 10px; border-radius: ${radius_sm}px; background: transparent; }
QMenuBar::item:selected { background: $hover; }
/* ---- toggles ----------------------------------------------------------- */
QCheckBox, QRadioButton { spacing: 8px; background: transparent; }
QCheckBox::indicator, QRadioButton::indicator {
width: 16px; height: 16px; background: $surface_raised;
border: 1px solid $border_strong; border-radius: ${radius_sm}px;
}
QRadioButton::indicator { border-radius: 9px; }
QCheckBox::indicator:hover, QRadioButton::indicator:hover { border-color: $accent; }
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
background: $accent_solid; border-color: $accent_solid;
}
QCheckBox::indicator:disabled, QRadioButton::indicator:disabled {
background: $surface; border-color: $border;
}
QCheckBox::indicator:checked:disabled, QRadioButton::indicator:checked:disabled {
background: $border_strong; border-color: $border_strong;
}
QSlider::groove:horizontal { height: 4px; background: $border; border-radius: 2px; }
QSlider::sub-page:horizontal { background: $accent_solid; border-radius: 2px; }
QSlider::handle:horizontal {
width: 14px; height: 14px; margin: -6px 0; border-radius: 7px;
background: $surface_raised; border: 1px solid $border_strong;
}
QSlider::handle:horizontal:hover { border-color: $accent; }
QProgressBar {
background: $surface; border: none; border-radius: 3px;
height: 6px; text-align: center; color: $text_muted;
}
QProgressBar::chunk { background: $accent_solid; border-radius: 3px; }
/* ---- scrollbars: overlay-thin, no arrows. ------------------------------ */
QScrollBar:vertical { background: transparent; width: 10px; margin: 2px; }
QScrollBar::handle:vertical { background: $scroll_handle; border-radius: 4px; min-height: 28px; }
QScrollBar::handle:vertical:hover { background: $scroll_handle_hover; }
QScrollBar:horizontal { background: transparent; height: 10px; margin: 2px; }
QScrollBar::handle:horizontal { background: $scroll_handle; border-radius: 4px; min-width: 28px; }
QScrollBar::handle:horizontal:hover { background: $scroll_handle_hover; }
QScrollBar::add-line, QScrollBar::sub-line { height: 0; width: 0; border: none; background: none; }
QScrollBar::add-page, QScrollBar::sub-page { background: none; }
/* ---- badges & inline text tones ---------------------------------------
One shape, six tones. Pick by meaning: badgeSuccess for a finished run,
badgeDanger for a failed one — not by which colour looks nice. */
QLabel#badge, QLabel#badgeSuccess, QLabel#badgeWarn, QLabel#badgeDanger,
QLabel#badgePurple, QLabel#badgePink {
border-radius: ${radius_sm}px; padding: 2px 8px; font-size: 12px; font-weight: 600;
}
QLabel#badge { background: $info_soft; color: $info; }
QLabel#badgeSuccess { background: $success_soft; color: $success; }
QLabel#badgeWarn { background: $warning_soft; color: $warning; }
QLabel#badgeDanger { background: $danger_soft; color: $danger; }
QLabel#badgePurple { background: $purple_soft; color: $purple; }
QLabel#badgePink { background: $pink_soft; color: $pink; }
QLabel { background: transparent; }
QLabel#hint { color: $text_muted; }
QLabel#faint { color: $text_faint; }
QLabel#warning { color: $warning; font-weight: 600; }
QLabel#error { color: $danger; font-weight: 600; }
QLabel#success { color: $success; font-weight: 600; }
QLabel#sectionTitle { color: $text; font-size: 15px; font-weight: 600; }
/* ---- code, terminals & logs -------------------------------------------
These read as "sunken" surfaces: the eye goes in, not across. */
QPlainTextEdit#codeEditor, QPlainTextEdit#termOutput, QPlainTextEdit#logView {
background: $code_bg; color: $code_fg; border: none;
font-family: $font_mono; selection-background-color: $code_selection;
}
QLineEdit#termInput {
background: $code_bg; color: $code_fg; border: none; border-top: 1px solid $border;
font-family: $font_mono; border-radius: 0; padding: 7px 10px;
}
QLineEdit#termInput:focus { border-top-color: $accent; }
/* The help-agent dock styles itself from these same tokens — it is a floating
overlay that re-applies on every theme switch. See ui/help_agent_widget.py. */
""")
def resolve_theme(theme: str) -> str:
"""Resolve ``'system'`` to ``'dark'``/``'light'`` from the OS colour scheme."""
if theme in _PALETTES:
return theme
try:
from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication
app = QApplication.instance()
if app is not None:
scheme = app.styleHints().colorScheme()
return "light" if scheme == Qt.ColorScheme.Light else "dark"
except Exception:
pass
return "dark"
def palette(theme: str) -> Palette:
"""The token set for ``theme``. Painting code reads its colours from here."""
return _PALETTES[resolve_theme(theme)]
# The theme the running app is currently showing. Painting code (paintEvent,
# QSyntaxHighlighter, canvas items) reads it via current_palette() instead of
# re-reading config.json — that used to cost a file open per repaint.
_active_theme = "dark"
def set_active_theme(theme: str) -> str:
"""Record the theme the app just applied. Call this next to every
``QApplication.setStyleSheet(stylesheet(...))``. Returns the resolved name."""
global _active_theme
_active_theme = resolve_theme(theme)
return _active_theme
def current_theme() -> str:
"""The resolved theme ('dark'/'light') the app is showing right now."""
return _active_theme
def current_palette() -> Palette:
"""Tokens for the theme the app is showing right now."""
return _PALETTES[_active_theme]
def stylesheet(theme: str) -> str:
"""The application-wide Qt style sheet for ``theme``."""
return _TEMPLATE.substitute(asdict(palette(theme)))
def role_colors(theme: str) -> dict[str, str]:
"""Conversation/graph node colours keyed by role."""
p = palette(theme)
return {
"user": p.role_user,
"assistant": p.role_assistant,
"tool": p.role_tool,
"result": p.role_result,
"error": p.role_error,
}