Skip to content
Closed
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
2 changes: 1 addition & 1 deletion build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ rmSync(outdir, { recursive: true, force: true });

// Default features that match the official CLI build.
// Additional features can be enabled via FEATURE_<NAME>=1 env vars.
const DEFAULT_BUILD_FEATURES = ["AGENT_TRIGGERS_REMOTE", "CHICAGO_MCP", "VOICE_MODE"];
const DEFAULT_BUILD_FEATURES = ["AGENT_TRIGGERS_REMOTE", "CHICAGO_MCP", "VOICE_MODE", "BUDDY"];

// Collect FEATURE_* env vars → Bun.build features
const envFeatures = Object.keys(process.env)
Expand Down
89 changes: 65 additions & 24 deletions packages/color-diff-napi/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ type Theme = {

function defaultSyntaxThemeName(themeName: string): string {
if (themeName.includes('ansi')) return 'ansi'
if (themeName.includes('dark')) return 'Monokai Extended'
if (themeName.includes('dark')) return 'Royal Gold Dark'
return 'GitHub'
}

Expand Down Expand Up @@ -221,6 +221,35 @@ const MONOKAI_SCOPES: Record<string, Color> = {
subst: rgb(248, 248, 242),
}

// Custom dark theme for the TUI: lower saturation, richer gold accents, and
// cooler blue-green contrast so code feels more refined on black backgrounds.
const ROYAL_GOLD_DARK_SCOPES: Record<string, Color> = {
keyword: rgb(254, 200, 74),
_storage: rgb(135, 195, 255),
built_in: rgb(135, 195, 255),
type: rgb(135, 195, 255),
literal: rgb(224, 164, 88),
number: rgb(224, 164, 88),
string: rgb(246, 224, 176),
title: rgb(235, 200, 141),
'title.function': rgb(235, 200, 141),
'title.class': rgb(235, 200, 141),
'title.class.inherited': rgb(235, 200, 141),
params: rgb(243, 240, 232),
comment: rgb(139, 125, 107),
meta: rgb(139, 125, 107),
attr: rgb(135, 195, 255),
attribute: rgb(135, 195, 255),
variable: rgb(243, 240, 232),
'variable.language': rgb(243, 240, 232),
property: rgb(243, 240, 232),
operator: rgb(231, 185, 76),
punctuation: rgb(229, 223, 211),
symbol: rgb(224, 164, 88),
regexp: rgb(246, 224, 176),
subst: rgb(229, 223, 211),
}

// highlight.js scope → syntect GitHub-light foreground (measured from Rust)
const GITHUB_SCOPES: Record<string, Color> = {
keyword: rgb(167, 29, 93),
Expand Down Expand Up @@ -286,6 +315,18 @@ const ANSI_SCOPES: Record<string, Color> = {
meta: ansiIdx(8),
}

// Brand colors for diff highlighting
const BRAND_DIFF_RED = rgb(162, 0, 67)
const BRAND_DIFF_GREEN = rgb(34, 139, 34)
const BRAND_DIFF_RED_DARK_LINE = rgb(92, 0, 38)
const BRAND_DIFF_RED_DARK_WORD = rgb(132, 0, 54)
const BRAND_DIFF_GREEN_DARK_LINE = rgb(10, 74, 41)
const BRAND_DIFF_GREEN_DARK_WORD = rgb(16, 110, 60)
const BRAND_DIFF_RED_LIGHT_LINE = rgb(242, 220, 230)
const BRAND_DIFF_RED_LIGHT_WORD = rgb(228, 170, 196)
const BRAND_DIFF_GREEN_LIGHT_LINE = rgb(220, 238, 220)
const BRAND_DIFF_GREEN_LIGHT_WORD = rgb(170, 214, 170)

function buildTheme(themeName: string, mode: ColorMode): Theme {
const isDark = themeName.includes('dark')
const isAnsi = themeName.includes('ansi')
Expand All @@ -308,57 +349,57 @@ function buildTheme(themeName: string, mode: ColorMode): Theme {

if (isDark) {
const fg = rgb(248, 248, 242)
const deleteLine = rgb(61, 1, 0)
const deleteWord = rgb(92, 2, 0)
const deleteDecoration = rgb(220, 90, 90)
const deleteLine = BRAND_DIFF_RED_DARK_LINE
const deleteWord = BRAND_DIFF_RED_DARK_WORD
const deleteDecoration = BRAND_DIFF_RED
if (isDaltonized) {
return {
addLine: tc ? rgb(0, 27, 41) : ansiIdx(17),
addWord: tc ? rgb(0, 48, 71) : ansiIdx(24),
addDecoration: rgb(81, 160, 200),
deleteLine,
deleteWord,
deleteDecoration,
deleteLine: rgb(61, 1, 0),
deleteWord: rgb(92, 2, 0),
deleteDecoration: rgb(220, 90, 90),
foreground: fg,
background: DEFAULT_BG,
scopes: MONOKAI_SCOPES,
scopes: ROYAL_GOLD_DARK_SCOPES,
}
}
return {
addLine: tc ? rgb(2, 40, 0) : ansiIdx(22),
addWord: tc ? rgb(4, 71, 0) : ansiIdx(28),
addDecoration: rgb(80, 200, 80),
addLine: tc ? BRAND_DIFF_GREEN_DARK_LINE : BRAND_DIFF_GREEN_DARK_LINE,
addWord: tc ? BRAND_DIFF_GREEN_DARK_WORD : BRAND_DIFF_GREEN_DARK_WORD,
addDecoration: BRAND_DIFF_GREEN,
deleteLine,
deleteWord,
deleteDecoration,
foreground: fg,
background: DEFAULT_BG,
scopes: MONOKAI_SCOPES,
scopes: ROYAL_GOLD_DARK_SCOPES,
}
}

// light
const fg = rgb(51, 51, 51)
const deleteLine = rgb(255, 220, 220)
const deleteWord = rgb(255, 199, 199)
const deleteDecoration = rgb(207, 34, 46)
const deleteLine = BRAND_DIFF_RED_LIGHT_LINE
const deleteWord = BRAND_DIFF_RED_LIGHT_WORD
const deleteDecoration = BRAND_DIFF_RED
if (isDaltonized) {
return {
addLine: rgb(219, 237, 255),
addWord: rgb(179, 217, 255),
addDecoration: rgb(36, 87, 138),
deleteLine,
deleteWord,
deleteDecoration,
addLine: BRAND_DIFF_GREEN_LIGHT_LINE,
addWord: BRAND_DIFF_GREEN_LIGHT_WORD,
addDecoration: BRAND_DIFF_GREEN,
deleteLine: rgb(255, 220, 220),
deleteWord: rgb(255, 199, 199),
deleteDecoration: rgb(207, 34, 46),
foreground: fg,
background: DEFAULT_BG,
scopes: GITHUB_SCOPES,
}
}
return {
addLine: rgb(220, 255, 220),
addWord: rgb(178, 255, 178),
addDecoration: rgb(36, 138, 61),
addLine: BRAND_DIFF_GREEN_LIGHT_LINE,
addWord: BRAND_DIFF_GREEN_LIGHT_WORD,
addDecoration: BRAND_DIFF_GREEN,
deleteLine,
deleteWord,
deleteDecoration,
Expand Down
114 changes: 93 additions & 21 deletions src/commands/buddy/buddy.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import React from 'react'
import {
getCompanion,
rollWithSeed,
generateSeed,
} from '../../buddy/companion.js'
import { type StoredCompanion, RARITY_STARS } from '../../buddy/types.js'
import { renderSprite } from '../../buddy/sprites.js'
import { CompanionCard } from '../../buddy/CompanionCard.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { triggerCompanionReaction } from '../../buddy/companionReact.js'
import type { ToolUseContext } from '../../Tool.js'
Expand Down Expand Up @@ -67,6 +65,22 @@ function speciesLabel(species: string): string {
return species.charAt(0).toUpperCase() + species.slice(1)
}

function renderStats(stats: Record<string, number>): string {
const lines = [
'DEBUGGING',
'PATIENCE',
'CHAOS',
'WISDOM',
'SNARK',
].map(name => {
const val = stats[name] ?? 0
const filled = Math.round(val / 5)
const bar = '█'.repeat(filled) + '░'.repeat(20 - filled)
return ` ${name.padEnd(10)} ${bar} ${val}`
})
return lines.join('\n')
}

export async function call(
onDone: LocalJSXCommandOnDone,
context: ToolUseContext & LocalJSXCommandContext,
Expand All @@ -75,20 +89,61 @@ export async function call(
const sub = args?.trim().toLowerCase() ?? ''
const setState = context.setAppState

// ── /buddy off — mute companion ──
if (sub === 'off') {
// ── /buddy mute — mute companion ──
if (sub === 'mute') {
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: true }))
onDone('companion muted', { display: 'system' })
return null
}

// ── /buddy on — unmute companion ──
if (sub === 'on') {
// ── /buddy unmute — unmute companion ──
if (sub === 'unmute') {
saveGlobalConfig(cfg => ({ ...cfg, companionMuted: false }))
onDone('companion unmuted', { display: 'system' })
return null
}

// ── /buddy rehatch — re-roll a new companion (replaces existing) ──
if (sub === 'rehatch') {
const seed = generateSeed()
const r = rollWithSeed(seed)
const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy'
const personality =
SPECIES_PERSONALITY[r.bones.species] ?? 'Mysterious and code-savvy.'

const stored: StoredCompanion = {
name,
personality,
seed,
hatchedAt: Date.now(),
}

saveGlobalConfig(cfg => ({ ...cfg, companion: stored }))

const stars = RARITY_STARS[r.bones.rarity]
const sprite = renderSprite(r.bones, 0)
const shiny = r.bones.shiny ? ' ✨ Shiny!' : ''

const lines = [
'🎉 A new companion appeared!',
'',
...sprite,
'',
` ${name} the ${speciesLabel(r.bones.species)}${shiny}`,
` Rarity: ${stars} (${r.bones.rarity})`,
` Eye: ${r.bones.eye} Hat: ${r.bones.hat}`,
'',
` "${personality}"`,
'',
' Stats:',
renderStats(r.bones.stats),
'',
' Your old companion has been replaced!',
]
onDone(lines.join('\n'), { display: 'system' })
return null
}

// ── /buddy pet — trigger heart animation + auto unmute ──
if (sub === 'pet') {
const companion = getCompanion()
Expand Down Expand Up @@ -123,16 +178,29 @@ export async function call(
}

if (companion) {
// Return JSX card — matches official vc8 component
const lastReaction = context.getAppState?.()?.companionReaction
return React.createElement(CompanionCard, {
companion,
lastReaction,
onDone,
})
// Show text-based companion info with 20-char stats
const stars = RARITY_STARS[companion.rarity]
const sprite = renderSprite(companion, 0)
const shiny = companion.shiny ? ' ✨ Shiny!' : ''

const lines = [
...sprite,
'',
` ${companion.name} the ${speciesLabel(companion.species)}${shiny}`,
` Rarity: ${stars} (${companion.rarity})`,
` Eye: ${companion.eye} Hat: ${companion.hat}`,
companion.personality ? `\n "${companion.personality}"` : '',
'',
' Stats:',
renderStats(companion.stats),
'',
' Commands: /buddy pet /buddy mute /buddy unmute /buddy rehatch',
]
onDone(lines.join('\n'), { display: 'system' })
return null
}

// ── No companion → hatch ──
// ── No companion → auto hatch ──
const seed = generateSeed()
const r = rollWithSeed(seed)
const name = SPECIES_NAMES[r.bones.species] ?? 'Buddy'
Expand All @@ -150,19 +218,23 @@ export async function call(

const stars = RARITY_STARS[r.bones.rarity]
const sprite = renderSprite(r.bones, 0)
const shiny = r.bones.shiny ? ' \u2728 Shiny!' : ''
const shiny = r.bones.shiny ? ' Shiny!' : ''

const lines = [
'A wild companion appeared!',
'🎉 A wild companion appeared!',
'',
...sprite,
'',
`${name} the ${speciesLabel(r.bones.species)}${shiny}`,
`Rarity: ${stars} (${r.bones.rarity})`,
`"${personality}"`,
` ${name} the ${speciesLabel(r.bones.species)}${shiny}`,
` Rarity: ${stars} (${r.bones.rarity})`,
` Eye: ${r.bones.eye} Hat: ${r.bones.hat}`,
'',
` "${personality}"`,
'',
' Stats:',
renderStats(r.bones.stats),
'',
'Your companion will now appear beside your input box!',
'Say its name to get its take \u00b7 /buddy pet \u00b7 /buddy off',
' Your companion will now appear beside your input box!',
]
onDone(lines.join('\n'), { display: 'system' })
return null
Expand Down
4 changes: 2 additions & 2 deletions src/commands/buddy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { isBuddyLive } from '../../buddy/useBuddyNotification.js'
const buddy = {
type: 'local-jsx',
name: 'buddy',
description: 'Hatch a coding companion · pet, off',
argumentHint: '[pet|off]',
description: 'Coding companion · pet, rehatch, mute, unmute',
argumentHint: '[pet|rehatch|mute|unmute]',
immediate: true,
get isHidden() {
return !isBuddyLive()
Expand Down
2 changes: 1 addition & 1 deletion src/components/FullscreenLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,7 @@ export function FullscreenLayout({
ref={scrollRef}
flexGrow={1}
flexDirection="column"
paddingTop={padCollapsed ? 0 : 1}
paddingTop={0}
stickyScroll
>
<ScrollChromeContext value={chromeCtx}>
Expand Down
13 changes: 3 additions & 10 deletions src/components/HighlightedCode.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as React from 'react'
import { memo, useEffect, useMemo, useRef, useState } from 'react'
import { useSettings } from '../hooks/useSettings.js'
import {
Ansi,
Box,
Expand Down Expand Up @@ -34,20 +33,14 @@ export const HighlightedCode = memo(function HighlightedCode({
const ref = useRef<DOMElement>(null)
const [measuredWidth, setMeasuredWidth] = useState(width || DEFAULT_WIDTH)
const [theme] = useTheme()
const settings = useSettings()
const syntaxHighlightingDisabled =
settings.syntaxHighlightingDisabled ?? false

const colorFile = useMemo(() => {
if (syntaxHighlightingDisabled) {
return null
}
const ColorFile = expectColorFile()
if (!ColorFile) {
return null
}
return new ColorFile(code, filePath)
}, [code, filePath, syntaxHighlightingDisabled])
}, [code, filePath])

useEffect(() => {
if (!width && ref.current) {
Expand All @@ -69,7 +62,7 @@ export const HighlightedCode = memo(function HighlightedCode({
// line number (max_digits = lineCount.toString().length) + space. No marker
// column like the diff path. Wrap in <NoSelect> so fullscreen selection
// yields clean code without line numbers. Only split in fullscreen mode
// (~ DOM nodes + sliceAnsi cost); non-fullscreen uses terminal-native
// (~4x DOM nodes + sliceAnsi cost); non-fullscreen uses terminal-native
// selection where noSelect is meaningless.
const gutterWidth = useMemo(() => {
if (!isFullscreenEnvEnabled()) return 0
Expand All @@ -96,7 +89,7 @@ export const HighlightedCode = memo(function HighlightedCode({
code={code}
filePath={filePath}
dim={dim}
skipColoring={syntaxHighlightingDisabled}
skipColoring={false}
/>
)}
</Box>
Expand Down
Loading