diff --git a/.gitignore b/.gitignore index bc6a98c3..48afd05d 100644 --- a/.gitignore +++ b/.gitignore @@ -177,3 +177,7 @@ coverage/ .env.test.local .env.production.local .gstack/ + +# ClawCodex Desktop packaged installers (electron-builder output) +ui-desktop/release/ +ui-desktop/test-results/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e3c7bb8..250dc47b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- **ClawCodex Desktop — a native desktop app** (`ui-desktop/`). The full + agent in a polished Electron window: streaming chat with live tool + activity, approval prompts, session sidebar with resume, side-by-side + previews, and settings — no terminal required. + + ``` + clawcodex desktop + ``` + + launches it from a checkout (installs UI deps on first run). Under the + hood the app spawns `clawcodex serve` — a new loopback HTTP + WebSocket + gateway (`/api/*` REST + JSON-RPC `/api/ws`) that runs sessions on the + same in-process agent core as the TUI, so both surfaces share one config, + one session store, and one set of skills. Ported from the reference + desktop implementation and fully rebranded, with a claw-mark icon set, + staged in PRs #802/#803/#804. + ## [1.4.0] - 2026-08-02 ### Added diff --git a/README.md b/README.md index 5a6ca3bc..c68eb4d0 100644 --- a/README.md +++ b/README.md @@ -552,6 +552,22 @@ Use `clawcodex config` to check connection status and **See [FEATURE_LIST.md](FEATURE_LIST.md) for detailed feature status and PR guidelines.** +## 🖥️ ClawCodex Desktop + +The agent also ships as a **native desktop app** (`ui-desktop/`): streaming +chat with live tool activity and approval prompts, a session sidebar with +resume, side-by-side previews, and settings — no terminal required. It runs +on the same backend, config, and session store as the CLI/TUI. + +```bash +# from a source checkout (installs UI deps on first run) +clawcodex desktop +``` + +The app boots its own backend via `clawcodex serve` (loopback HTTP + +WebSocket gateway over the in-process agent core). See +[`ui-desktop/README.md`](ui-desktop/README.md) for development docs. + ## 🚀 Quick Start ### Install diff --git a/ui-desktop/assets/icon.icns b/ui-desktop/assets/icon.icns index b144e29d..13cb53db 100644 Binary files a/ui-desktop/assets/icon.icns and b/ui-desktop/assets/icon.icns differ diff --git a/ui-desktop/assets/icon.ico b/ui-desktop/assets/icon.ico index da04ccab..5974cb98 100644 Binary files a/ui-desktop/assets/icon.ico and b/ui-desktop/assets/icon.ico differ diff --git a/ui-desktop/assets/icon.png b/ui-desktop/assets/icon.png index 04322dde..cf1348ab 100644 Binary files a/ui-desktop/assets/icon.png and b/ui-desktop/assets/icon.png differ diff --git a/ui-desktop/public/apple-touch-icon.png b/ui-desktop/public/apple-touch-icon.png index ccbc72d3..d9a83ba4 100644 Binary files a/ui-desktop/public/apple-touch-icon.png and b/ui-desktop/public/apple-touch-icon.png differ diff --git a/ui-desktop/public/clawcodex-mark.png b/ui-desktop/public/clawcodex-mark.png index 74f5baaa..abd46c46 100644 Binary files a/ui-desktop/public/clawcodex-mark.png and b/ui-desktop/public/clawcodex-mark.png differ diff --git a/ui-desktop/src/app/command-palette/index.tsx b/ui-desktop/src/app/command-palette/index.tsx index 75d9bb9d..2d553af9 100644 --- a/ui-desktop/src/app/command-palette/index.tsx +++ b/ui-desktop/src/app/command-palette/index.tsx @@ -1029,7 +1029,7 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) { ] }) - // Apply a theme directly from the root search (e.g. "nous" → Nous). Live + // Apply a theme directly from the root search (e.g. "clawcodex"). Live // preview via keepOpen, mirroring the nested theme picker. If the theme // can't render the current light/dark mode, flip to the one it supports. result.push({ diff --git a/ui-desktop/src/components/onboarding/index.tsx b/ui-desktop/src/components/onboarding/index.tsx index 126f3363..52d0b5a2 100644 --- a/ui-desktop/src/components/onboarding/index.tsx +++ b/ui-desktop/src/components/onboarding/index.tsx @@ -399,7 +399,7 @@ function Header() { ) } -export const FEATURED_ID = 'nous' +export const FEATURED_ID = 'anthropic' const SHOW_ALL_KEY = 'clawcodex-onboarding-show-all-v1' const readShowAll = () => { diff --git a/ui-desktop/src/store/updates.test.ts b/ui-desktop/src/store/updates.test.ts index e5f1264a..00b214ea 100644 --- a/ui-desktop/src/store/updates.test.ts +++ b/ui-desktop/src/store/updates.test.ts @@ -134,7 +134,7 @@ describe('reportBackendContract', () => { }) it('dismisses the toast when the backend meets the contract', () => { - reportBackendContract(5) + reportBackendContract(1) expect(dismissSpy).toHaveBeenCalledWith('backend-contract-skew') expect(notifySpy).not.toHaveBeenCalled() }) @@ -142,17 +142,17 @@ describe('reportBackendContract', () => { it('warns when the backend is behind (or reports no contract)', () => { reportBackendContract(undefined) expect(notifySpy).toHaveBeenCalledTimes(1) - reportBackendContract(1) + reportBackendContract(0) expect(notifySpy).toHaveBeenCalledTimes(2) }) it('stays quiet on later session opens once the user closed it', () => { - reportBackendContract(1) + reportBackendContract(0) lastToast().onDismiss() // user closes it → cooldown starts notifySpy.mockClear() // Opening another pre-existing session re-runs the check within cooldown. - reportBackendContract(1) + reportBackendContract(0) expect(notifySpy).not.toHaveBeenCalled() }) @@ -160,22 +160,22 @@ describe('reportBackendContract', () => { vi.useFakeTimers() vi.setSystemTime(0) - reportBackendContract(1) + reportBackendContract(0) lastToast().onDismiss() notifySpy.mockClear() vi.setSystemTime(25 * 60 * 60 * 1000) // > 24h cooldown - reportBackendContract(1) + reportBackendContract(0) expect(notifySpy).toHaveBeenCalledTimes(1) }) it('clears the snooze once the backend catches up, so a regression warns again', () => { - reportBackendContract(1) + reportBackendContract(0) lastToast().onDismiss() notifySpy.mockClear() - reportBackendContract(5) // backend updated → satisfied, snooze cleared - reportBackendContract(4) // a later regression must warn immediately + reportBackendContract(1) // backend updated → satisfied, snooze cleared + reportBackendContract(0) // a later regression must warn immediately expect(notifySpy).toHaveBeenCalledTimes(1) }) }) diff --git a/ui-desktop/src/themes/backend-sync.ts b/ui-desktop/src/themes/backend-sync.ts index 72d434ea..ea0d87e2 100644 --- a/ui-desktop/src/themes/backend-sync.ts +++ b/ui-desktop/src/themes/backend-sync.ts @@ -57,9 +57,9 @@ export function ingestBackendSkin(skin: ClawCodexSkin | undefined | null, { appl } // `default` is "no opinion" on the PALETTE — the desktop keeps its own default - // (nous), so we never register a converted theme under `default`. It is still a + // (clawcodex), so we never register a converted theme under `default`. It is still a // valid apply TARGET though: a runtime switch back to `default` must repaint the - // desktop to its own default (setTheme normalizes `default` → nous). So we only + // desktop to its own default (setTheme normalizes `default` → clawcodex). So we only // skip the registry step here and let it flow through the apply logic below. // Built-in names (mono/slate/…) already have a hand-tuned desktop palette — we // never shadow it, but the name is still a valid apply target. diff --git a/ui-desktop/src/themes/context.tsx b/ui-desktop/src/themes/context.tsx index 6932c59c..af54827f 100644 --- a/ui-desktop/src/themes/context.tsx +++ b/ui-desktop/src/themes/context.tsx @@ -19,7 +19,7 @@ import { $activeGatewayProfile, normalizeProfileKey } from '@/store/profile' import { $backendThemes, $pendingSkinApply } from './backend-sync' import { hexToRgb, mix, readableOn } from './color' -import { BUILTIN_THEME_LIST, DEFAULT_SKIN_NAME, DEFAULT_TYPOGRAPHY, nousTheme } from './presets' +import { BUILTIN_THEME_LIST, DEFAULT_SKIN_NAME, DEFAULT_TYPOGRAPHY, clawcodexTheme } from './presets' import type { DesktopTheme, DesktopThemeColors } from './types' import { $userThemes, listAllThemes, resolveTheme } from './user-themes' @@ -35,7 +35,7 @@ const PROFILE_MODES_KEY = 'clawcodex-desktop-profile-modes-v1' // Last active profile, recorded so the boot-time paint can pick that profile's // theme before the gateway reports which profile actually launched. const LAST_PROFILE_KEY = 'clawcodex-desktop-active-profile-v1' -const RETIRED_SKINS = new Set(['nous-light', 'default', 'gold']) +const RETIRED_SKINS = new Set(['clawcodex-light', 'default', 'gold']) export type ThemeMode = 'light' | 'dark' | 'system' @@ -116,7 +116,7 @@ function synthLightColors(seed: DesktopTheme): DesktopThemeColors { /** Returns the seed palette for a given skin + mode (no overrides applied). */ export function getBaseColors(skinName: string, mode: 'light' | 'dark'): DesktopThemeColors { - const seed = resolveTheme(skinName) ?? nousTheme + const seed = resolveTheme(skinName) ?? clawcodexTheme if (mode === 'dark') { return seed.darkColors ?? seed.colors @@ -126,7 +126,7 @@ export function getBaseColors(skinName: string, mode: 'light' | 'dark'): Desktop } function deriveTheme(skinName: string, mode: 'light' | 'dark'): DesktopTheme { - const seed = resolveTheme(skinName) ?? nousTheme + const seed = resolveTheme(skinName) ?? clawcodexTheme return { ...seed, @@ -180,7 +180,7 @@ function applyTheme(theme: DesktopTheme, mode: 'light' | 'dark') { const root = document.documentElement const c = theme.colors - const typo = { ...DEFAULT_TYPOGRAPHY, ...nousTheme.typography, ...theme.typography } + const typo = { ...DEFAULT_TYPOGRAPHY, ...clawcodexTheme.typography, ...theme.typography } const rendered = renderedModeFor(c, mode) const isDark = rendered === 'dark' const midground = c.midground ?? c.ring @@ -300,7 +300,7 @@ interface ThemeContextValue { const SKIN_LIST = BUILTIN_THEME_LIST.map(({ name, label, description }) => ({ name, label, description })) const ThemeContext = createContext({ - theme: nousTheme, + theme: clawcodexTheme, themeName: DEFAULT_SKIN_NAME, mode: 'light', resolvedMode: 'light', diff --git a/ui-desktop/src/themes/presets.ts b/ui-desktop/src/themes/presets.ts index 66dd33e7..d72e7ee3 100644 --- a/ui-desktop/src/themes/presets.ts +++ b/ui-desktop/src/themes/presets.ts @@ -19,20 +19,20 @@ const SYSTEM_MONO = 'Menlo, Monaco, "SF Mono", "Courier Prime", monospace, ' + E export const DEFAULT_TYPOGRAPHY: DesktopThemeTypography = { fontSans: SYSTEM_SANS, fontMono: SYSTEM_MONO } -const NOUS_BLUE = '#0053FD' +const CLAWCODEX_BLUE = '#0053FD' const PSYCHE_BLUE = '#1540B1' const PSYCHE_WARM = '#FFE6CB' -const nousTint = (pct: number) => `color-mix(in srgb, ${NOUS_BLUE} ${pct}%, #FFFFFF)` -const nousTintTransparent = (pct: number) => `color-mix(in srgb, ${NOUS_BLUE} ${pct}%, transparent)` +const brandTint = (pct: number) => `color-mix(in srgb, ${CLAWCODEX_BLUE} ${pct}%, #FFFFFF)` +const brandTintTransparent = (pct: number) => `color-mix(in srgb, ${CLAWCODEX_BLUE} ${pct}%, transparent)` /** * Nous — canonical ClawCodex desktop identity. The palette keeps the current * glass geometry neutral, then lets the old bb/gui blue and psyche cream * return as accent seeds. */ -export const nousTheme: DesktopTheme = { - name: 'nous', +export const clawcodexTheme: DesktopTheme = { + name: 'clawcodex', label: 'Nous', description: 'Glass neutrals with Nous blue accents', colors: { @@ -40,27 +40,27 @@ export const nousTheme: DesktopTheme = { foreground: '#17171A', card: '#FFFFFF', cardForeground: '#17171A', - muted: nousTint(5), + muted: brandTint(5), mutedForeground: '#666678', popover: '#FFFFFF', popoverForeground: '#17171A', - primary: NOUS_BLUE, + primary: CLAWCODEX_BLUE, primaryForeground: '#FCFCFC', - secondary: nousTint(7), + secondary: brandTint(7), secondaryForeground: '#242432', - accent: nousTint(10), + accent: brandTint(10), accentForeground: '#202030', - border: nousTintTransparent(22), - input: nousTintTransparent(30), - ring: NOUS_BLUE, - midground: NOUS_BLUE, - composerRing: NOUS_BLUE, + border: brandTintTransparent(22), + input: brandTintTransparent(30), + ring: CLAWCODEX_BLUE, + midground: CLAWCODEX_BLUE, + composerRing: CLAWCODEX_BLUE, destructive: '#C72E4D', destructiveForeground: '#FFFFFF', sidebarBackground: '#F3F7FF', - sidebarBorder: nousTintTransparent(18), - userBubble: nousTint(6), - userBubbleBorder: nousTintTransparent(24) + sidebarBorder: brandTintTransparent(18), + userBubble: brandTint(6), + userBubbleBorder: brandTintTransparent(24) }, darkColors: { background: '#0D2F86', @@ -80,7 +80,7 @@ export const nousTheme: DesktopTheme = { border: '#3158AD', input: '#0B2566', ring: PSYCHE_WARM, - midground: NOUS_BLUE, + midground: CLAWCODEX_BLUE, composerRing: PSYCHE_WARM, destructive: '#C0473A', destructiveForeground: '#FEF2F2', @@ -277,7 +277,9 @@ export const slateTheme: DesktopTheme = { } export const BUILTIN_THEMES: Record = { - nous: nousTheme, + clawcodex: clawcodexTheme, + // Legacy alias: pre-rename stored prefs ("nous") resolve to the same theme. + nous: clawcodexTheme, midnight: midnightTheme, ember: emberTheme, mono: monoTheme, @@ -288,4 +290,4 @@ export const BUILTIN_THEMES: Record = { export const BUILTIN_THEME_LIST = Object.values(BUILTIN_THEMES) /** Skin used when nothing is persisted or the persisted name is retired. */ -export const DEFAULT_SKIN_NAME = 'nous' +export const DEFAULT_SKIN_NAME = 'clawcodex' diff --git a/ui-desktop/src/themes/types.ts b/ui-desktop/src/themes/types.ts index 3aefda3e..3862e982 100644 --- a/ui-desktop/src/themes/types.ts +++ b/ui-desktop/src/themes/types.ts @@ -91,7 +91,7 @@ export interface DesktopTheme { description: string /** Light palette (also reused for dark when `darkColors` is omitted). */ colors: DesktopThemeColors - /** Hand-tuned dark palette. Skins like `nous` ship one. */ + /** Hand-tuned dark palette. Skins like `clawcodex` ship one. */ darkColors?: DesktopThemeColors typography?: Partial /** Light-variant terminal ANSI palette (also the fallback for dark). */ diff --git a/ui-desktop/src/themes/use-skin-command.ts b/ui-desktop/src/themes/use-skin-command.ts index ac831ee0..52f12052 100644 --- a/ui-desktop/src/themes/use-skin-command.ts +++ b/ui-desktop/src/themes/use-skin-command.ts @@ -5,10 +5,10 @@ import { useTheme } from './context' // Retired skin names land on the canonical Nous skin so old muscle memory works. const ALIASES: Record = { ares: 'ember', - default: 'nous', - gold: 'nous', - clawcodex: 'nous', - 'nous-light': 'nous' + default: 'clawcodex', + gold: 'clawcodex', + clawcodex: 'clawcodex', + 'clawcodex-light': 'clawcodex' } export function useSkinCommand() {