Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ GitHub Releases page; `0.8.0` is the new starting line.

## Unreleased

- **Refreshed TUI theme and Catppuccin syntax highlighting.** The interface adopts a brand periwinkle/indigo accent (`#B3B9F4` dark / `#0B114E` light) with a reharmonized selection tint, and code blocks now highlight with Catppuccin Mocha (dark) / Latte (light), adaptive to the active theme — implemented as foreground-only Pygments styles with no new dependency. Markdown inline code and links render terminal-native cyan, blockquotes green, and ordered-list markers bright blue (so they adapt per terminal), and user messages sit on a neutral grey block instead of the prior blue tint.
- **Homebrew updater no longer no-ops or false-reports success.** `pythinker update` on a Homebrew install now runs `brew update` to refresh the tap before `brew upgrade`, so a stale local tap clone can't pin the old formula and silently no-op ("0.37.0 already installed"). After upgrading it re-checks the installed version via `brew list --versions` and reports a clear failure instead of "Updated successfully!" when the version did not actually advance.

## 0.38.0 (2026-06-08)
Expand Down
11 changes: 6 additions & 5 deletions src/pythinker_code/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,13 +602,14 @@ class TUIConfig(BaseModel):
description="Show a compact recap line after completed interactive shell turns.",
)
code_theme: str = Field(
default="pythinker-ansi",
default="catppuccin-adaptive",
description=(
"Syntax-highlighting theme for assistant code blocks. Default "
"'pythinker-ansi' keeps the terminal-adaptive, transparent look. "
"Set to any Pygments style name (e.g. 'monokai', 'material', "
"'dracula', 'one-dark') to render code fences with that style on a "
"solid dark background block."
"'catppuccin-adaptive' uses Catppuccin Mocha (dark UI) / Latte "
"(light UI) on the calm transparent block. Use "
"'catppuccin-mocha'/'catppuccin-latte' to pin one, 'pythinker-ansi' "
"for the terminal-native ANSI look, or any Pygments style name (e.g. "
"'monokai', 'dracula') to render on that style's solid background."
),
)
smooth_streaming: bool = Field(
Expand Down
7 changes: 5 additions & 2 deletions src/pythinker_code/ui/shell/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1878,11 +1878,14 @@ def _cancel_background_tasks(self) -> None:
self._background_tasks.clear()


# Palette transferred from the animated SVG (pythinker_animated.svg).
# Fixed brand palette transferred from the animated SVG (pythinker_animated.svg).
# These are the robot mark's identity colors and are intentionally
# theme-independent — do NOT wire them to TuiTokens (the logo must look the same
# in light/dark and must not shift with the accent).
_LOGO_NAVY = "#213853" # outline / chassis (head + body frame, mouth, neck)
_LOGO_FACE = "#F9F2F5" # face / chest interior (cream)
_LOGO_CORAL = "#EE9983" # antenna ball, ears, accent bits
_LOGO_IRIS = "#AFE3F1" # eye iris + chest button glow (light cyan)
_LOGO_IRIS = "#AFE3F1" # eye iris + chest button glow (brand cyan)

_LOGO = (
f" [{_LOGO_CORAL}]●[/]\n"
Expand Down
17 changes: 12 additions & 5 deletions src/pythinker_code/ui/shell/components/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,19 +227,26 @@ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderR
# its padding share one uniform dark block (Aider-style); otherwise keep
# the calm ``code_block_bg`` tint.
if isinstance(self.theme, str):
# Stock opt-in style (e.g. monokai): paint the panel and code with the
# style's own background so they form one uniform solid block.
panel_style = Syntax.get_theme(self.theme).get_background_style()
syntax_bg: str | None = None
else:
# Default path (Catppuccin adaptive / ANSI sentinel): keep the calm
# code_block_bg tint on the panel and render the syntax transparently
# so only its foreground colors land (the "skip background" approach).
panel_style = (
RichStyle(bgcolor=colors.code_block_bg) if colors.code_block_bg else RichStyle()
)
syntax_bg = "default"

syntax = Syntax(
code_text,
self.lexer_name,
theme=self.theme,
word_wrap=True,
padding=0,
background_color=None,
background_color=syntax_bg,
)
highlighted = syntax.highlight(code_text)
highlighted.rstrip()
Expand Down Expand Up @@ -283,10 +290,10 @@ def _markdown_style_overrides(theme: ThemeName | None = None) -> dict[str, RichS
"markdown.hr": RichStyle(color=colors.code_block_border),
"markdown.code_block": RichStyle(color=colors.inline_code),
"markdown.code_block.border": RichStyle(color=colors.code_block_border, bold=True),
# Bullets/numbers are structural, not "important words" — keep them muted
# so the accent is reserved for headings and bold text.
"markdown.item.bullet": RichStyle(color=colors.quote, bold=True),
"markdown.item.number": RichStyle(color=colors.quote, bold=True),
# Ordered markers take the bright-blue accent; unordered bullets stay
# muted (structural, not "important words").
"markdown.item.bullet": RichStyle(color=colors.unordered_marker, bold=True),
"markdown.item.number": RichStyle(color=colors.ordered_marker, bold=True),
}


Expand Down
12 changes: 5 additions & 7 deletions src/pythinker_code/ui/shell/motion.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,12 @@ def verb_spinner_style() -> Style:
return Style(color=Color.parse(base))


# Backwards-compatible dark-theme constants used by tests and older callers.
# These MUST stay in lockstep with the dark `activity_verb*` theme tokens so the
# shimmer renders the same warm ember ramp everywhere.
_SHIMMER_BASE = "#EE9983"
_SHIMMER_MID = "#F4B5A5"
_SHIMMER_HIGHLIGHT = "#FBD9CE"
# Dark-theme shimmer-ramp reference constants, sourced directly from the
# `activity_verb*` tokens so they can never drift out of lockstep with the theme.
_SHIMMER_BASE = get_tui_tokens("dark").activity_verb
_SHIMMER_MID = get_tui_tokens("dark").activity_verb_mid
_SHIMMER_HIGHLIGHT = get_tui_tokens("dark").activity_verb_highlight
_SHIMMER_INTERVAL_S = 0.15
_SPINNER_SILVER = "#B8C0CC"


def shimmer_spinner_style(elapsed_s: float, *, reduced_motion: bool = False) -> Style:
Expand Down
65 changes: 37 additions & 28 deletions src/pythinker_code/ui/theme.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,12 @@ def _task_browser_style_light() -> PTKStyle:
# ---------------------------------------------------------------------------


# Selection-row background (accent-family tint). Single source of truth for the
# prompt-toolkit completion/dialog selection styles below AND the `selected_bg`
# TuiTokens field — keep them wired so the two never drift.
_SELECTED_BG_DARK = "#21243B"
_SELECTED_BG_LIGHT = "#E7E9F9"

_PROMPT_STYLE_DARK = {
"bottom-toolbar": "noreverse",
# Input area — minimal: no background bar, only the prompt glyph is
Expand All @@ -159,10 +165,10 @@ def _task_browser_style_light() -> PTKStyle:
"slash-completion-menu.command": "fg:#F4F4F5",
"slash-completion-menu.command.match": "fg:#AFE3F1 bold",
"slash-completion-menu.meta": "fg:#A3A3A3",
"slash-completion-menu.command.current": "bg:#243C54 fg:#F4F4F5 bold",
"slash-completion-menu.command.match.current": "bg:#243C54 fg:#AFE3F1 bold",
"slash-completion-menu.meta.current": "bg:#243C54 fg:#A3A3A3",
"slash-completion-menu.row.current": "bg:#243C54",
"slash-completion-menu.command.current": f"bg:{_SELECTED_BG_DARK} fg:#F4F4F5 bold",
"slash-completion-menu.command.match.current": f"bg:{_SELECTED_BG_DARK} fg:#AFE3F1 bold",
"slash-completion-menu.meta.current": f"bg:{_SELECTED_BG_DARK} fg:#A3A3A3",
"slash-completion-menu.row.current": f"bg:{_SELECTED_BG_DARK}",
"file-completion-menu": "",
"file-completion-menu.marker": "fg:#2B3A52",
"file-completion-menu.marker.current": "fg:#AFE3F1 bold",
Expand All @@ -175,7 +181,7 @@ def _task_browser_style_light() -> PTKStyle:
"shell-dialog.title": "fg:#F4F4F5 bold",
"shell-dialog.border": "fg:#2B3A52",
"shell-dialog.option": "fg:#A3A3A3",
"shell-dialog.option.current": "bg:#243C54 fg:#F4F4F5 bold",
"shell-dialog.option.current": f"bg:{_SELECTED_BG_DARK} fg:#F4F4F5 bold",
"shell-footer.key": "fg:#AFE3F1 bold",
"shell-footer.meta": "fg:#A3A3A3",
"shell-footer.warning": "fg:#E6B450",
Expand All @@ -196,10 +202,10 @@ def _task_browser_style_light() -> PTKStyle:
"slash-completion-menu.command": "fg:#4b5563",
"slash-completion-menu.command.match": "fg:#176B7E bold",
"slash-completion-menu.meta": "fg:#666666",
"slash-completion-menu.command.current": "bg:#E6F2F6 fg:#213853 bold",
"slash-completion-menu.command.match.current": "bg:#E6F2F6 fg:#176B7E bold",
"slash-completion-menu.meta.current": "bg:#E6F2F6 fg:#666666",
"slash-completion-menu.row.current": "bg:#E6F2F6",
"slash-completion-menu.command.current": f"bg:{_SELECTED_BG_LIGHT} fg:#213853 bold",
"slash-completion-menu.command.match.current": f"bg:{_SELECTED_BG_LIGHT} fg:#176B7E bold",
"slash-completion-menu.meta.current": f"bg:{_SELECTED_BG_LIGHT} fg:#666666",
"slash-completion-menu.row.current": f"bg:{_SELECTED_BG_LIGHT}",
"file-completion-menu": "",
"file-completion-menu.marker": "fg:#8A93A0",
"file-completion-menu.marker.current": "fg:#176B7E bold",
Expand All @@ -212,7 +218,7 @@ def _task_browser_style_light() -> PTKStyle:
"shell-dialog.title": "fg:#213853 bold",
"shell-dialog.border": "fg:#C8BEC0",
"shell-dialog.option": "fg:#666666",
"shell-dialog.option.current": "bg:#E6F2F6 fg:#213853 bold",
"shell-dialog.option.current": f"bg:{_SELECTED_BG_LIGHT} fg:#213853 bold",
"shell-footer.key": "fg:#176B7E bold",
"shell-footer.meta": "fg:#666666",
"shell-footer.warning": "fg:#9A6B18",
Expand Down Expand Up @@ -279,6 +285,8 @@ class MarkdownColors:
inline_code: str
link: str
quote: str
ordered_marker: str
unordered_marker: str
table_border: str
code_block_border: str
code_block_bg: str
Expand All @@ -287,18 +295,22 @@ class MarkdownColors:
spinner_failed: str


# Markdown/report role mapping: prose-heavy output stays professional and
# low-chrome. Headings/strong text use primary text, emphasis/quotes use muted
# grey, code/links use blue, and status accents stay green/red.
# All values are derived from TuiTokens so there is a single source of truth.
# Markdown/report role mapping. Headings/strong use primary text, emphasis and
# unordered bullets use muted grey, status accents stay green/red — all derived
# from TuiTokens. The four enumerated elements (inline code, links, blockquotes,
# ordered-list markers) instead use terminal-native ANSI names so they adapt to
# the user's terminal palette in both light and dark modes (see the design spec
# 2026-06-08).
def _build_markdown_colors(tokens: TuiTokens) -> MarkdownColors:
return MarkdownColors(
heading=tokens.tool_title,
emphasis=tokens.muted,
strong=tokens.tool_title,
inline_code=tokens.info,
link=tokens.info,
quote=tokens.muted,
inline_code="cyan", # terminal-native ANSI cyan
link="cyan", # cyan, rendered underlined
quote="green", # terminal-native ANSI green
ordered_marker="bright_blue", # ordered markers take the bright-blue accent
unordered_marker=tokens.muted, # unordered bullets stay muted
table_border=tokens.border_muted,
code_block_border=tokens.border_muted,
code_block_bg=tokens.code_block_bg,
Expand Down Expand Up @@ -453,7 +465,6 @@ class TuiTokens:
custom_message_text: str
custom_message_label: str
tool_pending_bg: str
tool_success_bg: str
tool_error_bg: str
tool_title: str
tool_output: str
Expand All @@ -471,9 +482,9 @@ class TuiTokens:


_TUI_TOKENS_DARK = TuiTokens(
accent="#5EA7E8",
accent="#B3B9F4",
border="#3A506D",
border_accent="#AFE3F1",
border_accent="#7C88DE",
border_muted="#2B3A52",
info="#AFE3F1",
success="#7BC97F",
Expand All @@ -488,14 +499,13 @@ class TuiTokens:
activity_verb_mid="#F4B5A5",
activity_verb_highlight="#FBD9CE",
activity_spinner="#B8C0CC",
selected_bg="#243C54",
user_message_bg="#1B2738",
selected_bg=_SELECTED_BG_DARK,
user_message_bg="#333333",
user_message_text="",
custom_message_bg="#16242E",
custom_message_text="",
custom_message_label="#AFE3F1",
tool_pending_bg="#1B2230",
tool_success_bg="#16271C",
tool_error_bg="#2E1D24",
tool_title="#F4F4F5",
tool_output="#A3A3A3",
Expand All @@ -508,9 +518,9 @@ class TuiTokens:


_TUI_TOKENS_LIGHT = TuiTokens(
accent="#256EA8",
accent="#0B114E",
border="#495F7C",
border_accent="#176B7E",
border_accent="#3B469B",
border_muted="#C8BEC0",
info="#176B7E",
success="#2C7A39",
Expand All @@ -525,14 +535,13 @@ class TuiTokens:
activity_verb_mid="#B0573C",
activity_verb_highlight="#8F3A26",
activity_spinner="#6B7280",
selected_bg="#E6F2F6",
user_message_bg="#F0E4E4",
selected_bg=_SELECTED_BG_LIGHT,
user_message_bg="#E0E0E0",
user_message_text="",
custom_message_bg="#E6F2F6",
custom_message_text="",
custom_message_label="#176B7E",
tool_pending_bg="#EFE7E8",
tool_success_bg="#E4F0E6",
tool_error_bg="#F6E3E3",
tool_title="#213853",
tool_output="#666666",
Expand Down
12 changes: 7 additions & 5 deletions src/pythinker_code/utils/rich/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,9 @@ def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bo

def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
render_options = options.update(width=options.max_width - 4)
style = self.style.without_color
# Keep the blockquote's own colour (blockquotes render green); the
# ``▌`` bar and quoted text share it.
style = self.style
lines = console.render_lines(self.elements, render_options, style=style)
new_line = Segment("\n")
padding = Segment("▌ ", style)
Expand Down Expand Up @@ -478,7 +480,8 @@ def on_child_close(self, context: MarkdownContext, child: MarkdownElement) -> bo
def render_bullet(self, console: Console, options: ConsoleOptions) -> RenderResult:
indent_padding_len = LIST_INDENT_WIDTH * self.indent
indent_text = " " * indent_padding_len
bullet = Segment("• ")
bullet_style = console.get_style("markdown.item.bullet", default="none")
bullet = Segment("• ", bullet_style)
new_line = Segment("\n")
bullet_width = cell_len(bullet.text)
child_width = max(1, options.max_width - indent_padding_len - bullet_width)
Expand Down Expand Up @@ -513,7 +516,8 @@ def render_number(
indent_padding_len = LIST_INDENT_WIDTH * self.indent
indent_text = " " * indent_padding_len
numeral_text = f"{number}. "
numeral = Segment(numeral_text)
number_style = console.get_style("markdown.item.number", default="none")
numeral = Segment(numeral_text, number_style)
numeral_width = cell_len(numeral_text)
child_width = max(1, options.max_width - indent_padding_len - numeral_width)
lines = console.render_lines(
Expand Down Expand Up @@ -637,8 +641,6 @@ def enter_style(self, style_name: str | Style) -> Style:
style = self.console.get_style(style_name, default=fallback)
style = fallback + style
style = style.copy()
if isinstance(style_name, str) and style_name == "markdown.block_quote":
style = style.without_color
if (
isinstance(style_name, str)
and style_name in {"markdown.code", "markdown.code_block"}
Expand Down
Loading
Loading