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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file modified ui-desktop/assets/icon.icns
Binary file not shown.
Binary file modified ui-desktop/assets/icon.ico
Binary file not shown.
Binary file modified ui-desktop/assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ui-desktop/public/apple-touch-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified ui-desktop/public/clawcodex-mark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion ui-desktop/src/app/command-palette/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion ui-desktop/src/components/onboarding/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down
18 changes: 9 additions & 9 deletions ui-desktop/src/store/updates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,48 +134,48 @@ 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()
})

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()
})

it('reminds again after the cooldown elapses', () => {
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)
})
})
Expand Down
4 changes: 2 additions & 2 deletions ui-desktop/src/themes/backend-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
12 changes: 6 additions & 6 deletions ui-desktop/src/themes/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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'

Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -300,7 +300,7 @@ interface ThemeContextValue {
const SKIN_LIST = BUILTIN_THEME_LIST.map(({ name, label, description }) => ({ name, label, description }))

const ThemeContext = createContext<ThemeContextValue>({
theme: nousTheme,
theme: clawcodexTheme,
themeName: DEFAULT_SKIN_NAME,
mode: 'light',
resolvedMode: 'light',
Expand Down
42 changes: 22 additions & 20 deletions ui-desktop/src/themes/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,48 +19,48 @@ 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: {
background: '#F8FAFF',
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',
Expand All @@ -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',
Expand Down Expand Up @@ -277,7 +277,9 @@ export const slateTheme: DesktopTheme = {
}

export const BUILTIN_THEMES: Record<string, DesktopTheme> = {
nous: nousTheme,
clawcodex: clawcodexTheme,
// Legacy alias: pre-rename stored prefs ("nous") resolve to the same theme.
nous: clawcodexTheme,
midnight: midnightTheme,
ember: emberTheme,
mono: monoTheme,
Expand All @@ -288,4 +290,4 @@ export const BUILTIN_THEMES: Record<string, DesktopTheme> = {
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'
2 changes: 1 addition & 1 deletion ui-desktop/src/themes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<DesktopThemeTypography>
/** Light-variant terminal ANSI palette (also the fallback for dark). */
Expand Down
8 changes: 4 additions & 4 deletions ui-desktop/src/themes/use-skin-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
ares: 'ember',
default: 'nous',
gold: 'nous',
clawcodex: 'nous',
'nous-light': 'nous'
default: 'clawcodex',
gold: 'clawcodex',
clawcodex: 'clawcodex',
'clawcodex-light': 'clawcodex'
}

export function useSkinCommand() {
Expand Down
Loading