From 884207c59e824efdf9c206aa17fd7c5f2181dd22 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 3 Jun 2026 16:51:06 +0200 Subject: [PATCH] add Catppuccin Mocha theme (+ high-contrast sibling) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ships catppuccin-mocha (faithful to the official Catppuccin Mocha palette) and catppuccin-mocha-hc (high-contrast sibling per D-69 — the faithful palette is never retuned to pass the gate). Both bundled and gated: mocha clears baseline AA, mocha-hc clears the strict extended set. Catppuccin MIT palette recorded in licenses.yaml. Closes T-82 (Mocha + hc meets every acceptance bullet; faithful Latte deferred — it can't clear baseline AA without retuning, a separate call). Co-Authored-By: Claude Opus 4.8 (1M context) --- .pql/changelog/ticket_history/2026-06.sql | 4 ++ .pql/changelog/tickets/2026-06.sql | 55 +++++++++++++++++++ CHANGELOG.md | 3 + assets/licenses.yaml | 11 ++++ .../src/theme/themes/catppuccin-mocha-hc.yaml | 40 ++++++++++++++ .../src/theme/themes/catppuccin-mocha.yaml | 39 +++++++++++++ lib/main.dart | 2 + test/a11y/contrast_test.dart | 2 + 8 files changed, 156 insertions(+) create mode 100644 lib/kernel/src/theme/themes/catppuccin-mocha-hc.yaml create mode 100644 lib/kernel/src/theme/themes/catppuccin-mocha.yaml diff --git a/.pql/changelog/ticket_history/2026-06.sql b/.pql/changelog/ticket_history/2026-06.sql index ce353a17..9041c09d 100644 --- a/.pql/changelog/ticket_history/2026-06.sql +++ b/.pql/changelog/ticket_history/2026-06.sql @@ -112,3 +112,7 @@ INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, Resolved (2026-06-03): NO new mechanism or decision needed -- the contract already existed. D-74''s CommandSchema.normalize (lib/src/ipc/command_schema.dart) already folds the argv shape {positional,flags} into named args using a declared positional ordering, and the dispatcher already runs normalize+validate. The arg-taking commands simply never registered a schema (adoption is opt-in per D-74). Fix = adopt it: added positional schemas (non-required, so missing-arg errors stay as handlers produce them; only effect is positional->named mapping + numeric coercion of line/cols/rows) to editor.open/activate/read/save/close, files.read/ls, pane.close/focus/resize/write. Handlers unchanged. Tests: CLI-shape ({positional:[...]}) dispatch now binds (editor/pane/files command tests). DEFERRED (still named-arg only; in-process UI works, CLI-arg niche): editor.insert/replace-selection/set-selection/set-content (content/selection encoding + active-buffer fallback make positional ambiguous), pane.spawn (argv is a list, not scalar positional), git.* arg verbs (agents use plain git; D-83 external-agent scope). NOTE: takes effect on app RESTART, not hot reload -- the dispatcher is built once at boot.', NULL, '2026-06-03 13:41:38', '2026-06-03 13:41:38', '2026-06-03 13:41:38', NULL, '0655a8efb4c977b404daedd86d926761', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-232', 'status', 'in_progress', 'done', NULL, '2026-06-03 13:41:42', '2026-06-03 13:41:42', '2026-06-03 13:41:42', NULL, '17995cc58fd4260f4901357531eeafc4', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-234', 'status', 'backlog', 'ready', NULL, '2026-06-03 14:25:47', '2026-06-03 14:25:47', '2026-06-03 14:25:47', NULL, 'e8216367ab4337bdbaa8fc97c596e111', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-234', 'status', 'ready', 'in_progress', NULL, '2026-06-03 14:27:26', '2026-06-03 14:27:26', '2026-06-03 14:27:26', NULL, '3db43a5f8b3855b2a78b4f6294dfeb97', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-82', 'status', 'ready', 'in_progress', NULL, '2026-06-03 14:27:26', '2026-06-03 14:27:26', '2026-06-03 14:27:26', NULL, '7a7042a498a0f09a072d911fad8d33d7', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-230', 'status', 'ready', 'in_progress', NULL, '2026-06-03 14:27:26', '2026-06-03 14:27:26', '2026-06-03 14:27:26', NULL, 'ec0e54053e51dea1fecb48eebc69e387', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-06.sql b/.pql/changelog/tickets/2026-06.sql index 1fe88038..fa852936 100644 --- a/.pql/changelog/tickets/2026-06.sql +++ b/.pql/changelog/tickets/2026-06.sql @@ -303,3 +303,58 @@ Acceptance: 3. Selecting a theme applies it live (ThemeController.select) and closes the popout. 4. Esc / outside-click dismisses without changing the theme; the control is keyboard-operable. 5. theme.pick (palette/command) still works unchanged.', 'backlog', 'medium', NULL, NULL, NULL, '2026-06-03 13:49:44', '2026-06-03 13:49:44', NULL, '301e958c679661deb77d1e3ca5d45d72', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-230', 'story', 'T-132', 'Cluster meta messages into a collapsible activity card', 'The Claude pane renders every transcript item as its own row, so a heavy agent turn becomes a wall of tool-call/result rows (Bash / Bash · result {…} / ''completed with no output'') that buries the messages that matter (user + Claude prose). Wireframe: docs/design/wireframes/claude/meta-activity-card.png. + +Fold runs of consecutive ''meta'' items into one live, collapsible activity card: +- The card shows the MOST RECENT meta line as a live ticker and updates in place as new ones stream in — a running ''what''s happening now''. A step count (''14 steps'') sits on the right. +- Collapsed by default; click the chevron to expand the full list of folded steps, click to re-collapse. +- ''Sticky'' items never fold — they render first-class. A sticky item SEALS the current card and a new cluster begins after it. A cluster = one unbroken run of foldable items between two sticky items. + +Taxonomy over the sealed ConversationItem set (transcript_reader.dart): UserMessage, AssistantTextMessage, AssistantThinkingMessage, AssistantToolUse, ToolResultMessage. +- Always sticky: UserMessage, AssistantTextMessage. Permission/AskUserQuestion prompts already live in the interaction zone (D-78) and are inherently sticky / cluster-breaking — no change, but note they break the run too. +- Foldable: AssistantToolUse + its paired ToolResultMessage (pairing already exists via toolUseById, T-168). + +Refinement decisions (2026-06-03, user): +1. WHAT FOLDS is a 3-level setting (conservative → aggressive); DEFAULT = level 1. Build the foldable predicate so all three are switchable: + - L1 (default): fold tool calls + results only; diffs (Edit/Write results) AND AssistantThinkingMessage stay first-class/sticky. + - L2: also fold thinking; diffs stay sticky. + - L3: fold everything except UserMessage + AssistantTextMessage (incl. diffs + thinking). +2. ERRORS: a failed/error ToolResultMessage SURFACES — treated as sticky, stays visible and breaks the cluster (see wireframe state B). Start here; the user noted some non-fatal errors are themselves clutter, so expect to tune which errors surface vs. fold. +3. DEFAULT/RUN STATE: always collapsed, showing the last line + step count, with the ticker updating live. NO setting. (Rejected auto-expanding the in-flight cluster: it scrolls the last useful sticky message out of view during long runs and only collapses when done — worst timing. Collapsed-with-live-ticker keeps the last sticky message anchored while preserving the motion/dynamism.) + +Implementation home: a ''collapse adjacent foldable items'' grouping pass over ConversationController.items, consumed by ConversationView; the card is a new conversation_card variant. Keep it accessible (keyboard expand/collapse + semantics: announce step count and collapsed/expanded state). + +Acceptance: +1. Consecutive foldable items collapse into one card; the collapsed card shows the latest line + a live-updating step count. +2. A sticky message (user / Claude prose / surfaced error / interaction-zone prompt) seals the card and starts a new cluster after it. +3. Expanding shows every folded step (tool call + result pairs) in order; collapsing returns to the one-line view. +4. The foldable level is a setting (default L1: diffs + thinking stay first-class); switching levels re-groups the transcript. +5. Failed tool results surface as their own (sticky) row and break the cluster. +6. Keyboard + screen-reader accessible (expand/collapse, step count announced).', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-03 11:15:39', '2026-06-03 14:27:26', NULL, '3eb3d504dad69cc0c06e57bce8f10456', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-82', 'task', NULL, 'add Catppuccin theme to bundled themes', 'User request: ship a Catppuccin theme alongside the four bundled themes (clide, midnight, paper, terminal — D-44). + +**Acceptance:** +- New theme YAML at `lib/kernel/src/theme/themes/catppuccin.yaml` (or split for the four Catppuccin flavours: latte / frappe / macchiato / mocha — pick one or all four; recommend Mocha as the dark default and Latte for the light variant). +- Tokens populated for every key in `SurfaceTokens` (lib/kernel/src/theme/tokens.dart) — chrome, panels, tabs, list items, status colours, syntax tokens (D-45). +- WCAG-AA contrast gate (D-22) passes against the theme. +- Listed in `assets/licenses.yaml` with the upstream license (MIT) and attribution. +- Theme picker (`builtin.theme-picker`) shows it in the live-switch menu. + +**Source palettes:** https://github.com/catppuccin/catppuccin#-palette — official hex values, don`t paraphrase. + +**Why now:** test user request; also a good exercise for any future user-contributed theme since Catppuccin is the most common one to ask for.', 'in_progress', 'low', NULL, NULL, NULL, '2026-05-06 08:30:15', '2026-06-03 14:27:26', NULL, '4cb876eb35abc00ee93eb0bced3315da', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-234', 'story', NULL, 'Theme-switcher popout in the status bar', 'Add a theme switcher to the far-right corner of the bottom status bar (the empty area to the right of the ''application ok'' / ipc-status item — see the user''s screenshot). Today switching themes is only reachable via the theme.pick command / palette (ThemePickerExtension opens a modal picker_view); there''s no always-visible affordance. + +Add a compact status-bar control that opens a POPOUT (popover anchored to the control, not the full-screen modal) for quick theme switching: +- A small right-aligned status item — a palette/swatch icon (or the current theme name). Contribute via StatusItemContribution at Slots.statusbar with priority > 100 so it sits in the right group, to the right of the ipc/toolchain ''application ok'' item (priority 100). +- Click (or focus + Enter/Space) opens a popover listing ThemeController.available, the current (ThemeController.currentName) marked. +- Selecting an entry applies it immediately via ThemeController.select(name) and closes the popout. +- Esc / click-away dismisses with no change. Keyboard navigable (arrow + Enter), matching the palette/quick-open overlay pattern. +- Natural home: ThemePickerExtension (lib/builtin/theme_picker). Reuse the theme list + select logic from picker_view rather than duplicating; the modal theme.pick command stays as-is for parity (D-6). + +Acceptance: +1. A theme control sits in the far-right of the status bar. +2. Clicking/activating it opens a popover of all available themes with the current one marked. +3. Selecting a theme applies it live (ThemeController.select) and closes the popout. +4. Esc / outside-click dismisses without changing the theme; the control is keyboard-operable. +5. theme.pick (palette/command) still works unchanged.', 'in_progress', 'medium', NULL, NULL, NULL, '2026-06-03 13:49:44', '2026-06-03 14:27:26', NULL, '9a5ac18d9e54e009f912b9c54584d796', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4fe48c..485ebe21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,9 @@ heading, and (b) bumping `pubspec.yaml` `version:` in the same commit. ### Added +- Catppuccin Mocha theme, plus a high-contrast `catppuccin-mocha-hc` sibling, + added to the bundled themes — switchable from the theme picker. Faithful to + the official palette (D-69). (T-82) - `clide ui open ` opens a doc in a GUI reader from the CLI — `tickets`/`decisions` by id, `markdown` by path — so an agent can surface what it's looking at on your screen. The drive-half complement to `clide status`. (T-231) diff --git a/assets/licenses.yaml b/assets/licenses.yaml index 5e78ca19..2c6b1fd2 100644 --- a/assets/licenses.yaml +++ b/assets/licenses.yaml @@ -81,6 +81,17 @@ dependencies: Fill weights bundled. weights_bundled: [Regular, Bold, Fill] + - name: Catppuccin (palette) + kind: theme-palette + version: "mocha" + homepage: https://github.com/catppuccin/catppuccin + license: MIT + purpose: >- + Source palette for the bundled catppuccin-mocha theme and its + high-contrast sibling catppuccin-mocha-hc. Official hex values, + mapped to clide's SurfaceTokens (Mauve accent, Blue primary); the + faithful palette is not retuned (D-69). + - name: yaml kind: dart-package version: "3.1.3" diff --git a/lib/kernel/src/theme/themes/catppuccin-mocha-hc.yaml b/lib/kernel/src/theme/themes/catppuccin-mocha-hc.yaml new file mode 100644 index 00000000..bb253b9f --- /dev/null +++ b/lib/kernel/src/theme/themes/catppuccin-mocha-hc.yaml @@ -0,0 +1,40 @@ +# catppuccin-mocha-hc — high-contrast sibling of `catppuccin-mocha` +# +# Same Catppuccin Mocha silhouette; muted text, status chips, syntax +# tokens, and the focus border bumped to clear the strict contrast gate +# (D-22 + D-69, `extendedPairs`). The faithful palette lives in +# catppuccin-mocha.yaml and is never retuned. License: MIT. + +name: catppuccin-mocha-hc +display_name: Catppuccin Mocha (high contrast) +dark: true + +palette: + background: "#1e1e2e" # Base + panel: "#181825" # Mantle + surface: "#313244" # Surface0 + muted: "#bac2de" # Subtext1 (brightened for AA) + foreground: "#eff1f5" # near-white + secondary: "#cdd6f4" # Text + primary: "#a6c8ff" # brightened Blue + accent: "#d4bbfb" # brightened Mauve + success: "#b9ecb4" # brightened Green + warning: "#f9e2af" # Yellow + error: "#ffa3bd" # brightened Red + info: "#9fe1ee" # brightened Sky + onAccent: "#11111b" # Crust + surfaceHi: "#45475a" # Surface1 + border: "#313244" # Surface0 + borderHi: "#9399b2" # Overlay2 (visible focus border) + textDim: "#bac2de" # Subtext1 + textMute: "#9399b2" # Overlay2 + accentSoft: "#21cba6f7" # Mauve, low alpha + +syntax: + keyword: "#cba6f7" # Mauve + type: "#f9e2af" # Yellow + string: "#a6e3a1" # Green + number: "#fab387" # Peach + comment: "#a6adc8" # Subtext0 (brightened for AA on the panel) + method: "#89b4fa" # Blue + punct: "#9399b2" # Overlay2 diff --git a/lib/kernel/src/theme/themes/catppuccin-mocha.yaml b/lib/kernel/src/theme/themes/catppuccin-mocha.yaml new file mode 100644 index 00000000..c274f1ad --- /dev/null +++ b/lib/kernel/src/theme/themes/catppuccin-mocha.yaml @@ -0,0 +1,39 @@ +# catppuccin-mocha — the Catppuccin Mocha (dark) flavour +# Source palette: https://github.com/catppuccin/catppuccin (official hex). +# Mapping: Base/Mantle/Surface0/1/2 → chrome; Text/Subtext → foreground; +# Blue=primary, Mauve=accent (the Catppuccin signature). License: MIT, +# see assets/licenses.yaml. + +name: catppuccin-mocha +display_name: Catppuccin Mocha +dark: true + +palette: + background: "#1e1e2e" # Base + panel: "#181825" # Mantle + surface: "#313244" # Surface0 + muted: "#9399b2" # Overlay2 + foreground: "#cdd6f4" # Text + secondary: "#bac2de" # Subtext1 + primary: "#89b4fa" # Blue + accent: "#cba6f7" # Mauve + success: "#a6e3a1" # Green + warning: "#f9e2af" # Yellow + error: "#f38ba8" # Red + info: "#89dceb" # Sky + onAccent: "#11111b" # Crust (dark text on the light accent) + surfaceHi: "#45475a" # Surface1 + border: "#313244" # Surface0 + borderHi: "#45475a" # Surface1 + textDim: "#a6adc8" # Subtext0 + textMute: "#7f849c" # Overlay1 + accentSoft: "#21cba6f7" # Mauve, low alpha + +syntax: + keyword: "#cba6f7" # Mauve + type: "#f9e2af" # Yellow + string: "#a6e3a1" # Green + number: "#fab387" # Peach + comment: "#7f849c" # Overlay1 + method: "#89b4fa" # Blue + punct: "#9399b2" # Overlay2 diff --git a/lib/main.dart b/lib/main.dart index ab5c0960..97fe97d0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -425,6 +425,8 @@ Future> _loadBundledThemes() async { 'lib/kernel/src/theme/themes/midnight-hc.yaml', 'lib/kernel/src/theme/themes/paper-hc.yaml', 'lib/kernel/src/theme/themes/terminal-hc.yaml', + 'lib/kernel/src/theme/themes/catppuccin-mocha.yaml', + 'lib/kernel/src/theme/themes/catppuccin-mocha-hc.yaml', ]; final out = []; for (final p in paths) { diff --git a/test/a11y/contrast_test.dart b/test/a11y/contrast_test.dart index d528b800..863ff634 100644 --- a/test/a11y/contrast_test.dart +++ b/test/a11y/contrast_test.dart @@ -23,6 +23,8 @@ void main() { 'lib/kernel/src/theme/themes/midnight-hc.yaml', 'lib/kernel/src/theme/themes/paper-hc.yaml', 'lib/kernel/src/theme/themes/terminal-hc.yaml', + 'lib/kernel/src/theme/themes/catppuccin-mocha.yaml', + 'lib/kernel/src/theme/themes/catppuccin-mocha-hc.yaml', ]; for (final path in bundledPaths) {