feat: upgrade to React 19 and Ink 6, fix Windows compatibility

- Upgrade React from 18.3.1 to 19.1.1
- Upgrade Ink from 5.2.1 to 6.2.3
- Fix Windows spawn EINVAL error by removing --tsconfig-raw
- Fix top-level await issues in cli.tsx
- Update build scripts for better Windows support
- Version bump to 1.1.12
This commit is contained in:
CrazyBoyM 2025-08-29 23:48:14 +08:00
parent 8ae7cb47ce
commit 487aef295d
26 changed files with 455 additions and 251 deletions

View File

@ -1,13 +1,13 @@
{ {
"name": "@shareai-lab/kode", "name": "@shareai-lab/kode",
"version": "1.0.82", "version": "1.1.12",
"bin": { "bin": {
"kode": "cli.js", "kode": "cli.js",
"kwa": "cli.js", "kwa": "cli.js",
"kd": "cli.js" "kd": "cli.js"
}, },
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=20.18.1"
}, },
"main": "cli.js", "main": "cli.js",
"author": "ShareAI-lab <ai-lab@foxmail.com>", "author": "ShareAI-lab <ai-lab@foxmail.com>",
@ -54,7 +54,7 @@
"@modelcontextprotocol/sdk": "^1.15.1", "@modelcontextprotocol/sdk": "^1.15.1",
"@statsig/js-client": "^3.18.2", "@statsig/js-client": "^3.18.2",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/react": "^19.1.8", "@types/react": "^19.1.12",
"ansi-escapes": "^7.0.0", "ansi-escapes": "^7.0.0",
"chalk": "^5.4.1", "chalk": "^5.4.1",
"cli-highlight": "^2.1.11", "cli-highlight": "^2.1.11",
@ -68,7 +68,7 @@
"glob": "^11.0.3", "glob": "^11.0.3",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"ink": "^5.2.1", "ink": "^6.2.3",
"ink-link": "^4.1.0", "ink-link": "^4.1.0",
"ink-select-input": "^6.2.0", "ink-select-input": "^6.2.0",
"ink-text-input": "^6.0.0", "ink-text-input": "^6.0.0",
@ -79,7 +79,7 @@
"node-fetch": "^3.3.2", "node-fetch": "^3.3.2",
"node-html-parser": "^7.0.1", "node-html-parser": "^7.0.1",
"openai": "^4.104.0", "openai": "^4.104.0",
"react": "18.3.1", "react": "^19.1.1",
"semver": "^7.7.2", "semver": "^7.7.2",
"shell-quote": "^1.8.3", "shell-quote": "^1.8.3",
"spawn-rx": "^5.1.2", "spawn-rx": "^5.1.2",

View File

@ -12,8 +12,9 @@ async function build() {
rmSync(file, { recursive: true, force: true }); rmSync(file, { recursive: true, force: true });
} }
}); });
// No dist artifacts; wrapper-only build
// Create the CLI wrapper // Create the CLI wrapper (exactly as in the referenced PR)
const wrapper = `#!/usr/bin/env node const wrapper = `#!/usr/bin/env node
const { spawn } = require('child_process'); const { spawn } = require('child_process');
@ -48,34 +49,50 @@ try {
} }
function runWithNode() { function runWithNode() {
// Use local tsx installation // Use local tsx installation; if missing, try PATH-resolved tsx
const tsxPath = path.join(__dirname, 'node_modules', '.bin', 'tsx'); const binDir = path.join(__dirname, 'node_modules', '.bin')
const child = spawn(tsxPath, [cliPath, ...args], { const tsxPath = process.platform === 'win32'
? path.join(binDir, 'tsx.cmd')
: path.join(binDir, 'tsx')
const runPathTsx = () => {
const child2 = spawn('tsx', [cliPath, ...args], {
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32',
env: { env: {
...process.env, ...process.env,
YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm') YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm'),
TSX_TSCONFIG_PATH: process.platform === 'win32' ? 'noop' : undefined
},
})
child2.on('error', () => {
console.error('\\nError: tsx is required but not found.')
console.error('Please install tsx globally: npm install -g tsx')
process.exit(1)
})
child2.on('exit', (code2) => process.exit(code2 || 0))
} }
});
child.on('error', (err) => { const child = spawn(tsxPath, [cliPath, ...args], {
if (err.code === 'ENOENT') { stdio: 'inherit',
console.error('\\nError: tsx is required but not found.'); shell: process.platform === 'win32',
console.error('Please run: npm install'); env: {
process.exit(1); ...process.env,
} else { YOGA_WASM_PATH: path.join(__dirname, 'yoga.wasm'),
console.error('Failed to start Kode:', err.message); TSX_TSCONFIG_PATH: process.platform === 'win32' ? 'noop' : undefined
process.exit(1); },
} })
});
child.on('exit', (code) => process.exit(code || 0)); child.on('error', () => runPathTsx())
child.on('exit', (code) => {
if (code && code !== 0) return runPathTsx()
process.exit(code || 0)
})
} }
`; `;
writeFileSync('cli.js', wrapper); writeFileSync('cli.js', wrapper);
chmodSync('cli.js', 0o755); chmodSync('cli.js', 0o755);
// Create .npmrc // Create .npmrc
const npmrc = `# Ensure tsx is installed const npmrc = `# Ensure tsx is installed
auto-install-peers=true auto-install-peers=true

View File

@ -1,56 +1,18 @@
#!/usr/bin/env node #!/usr/bin/env node
const { execSync } = require('child_process'); // This postinstall is intentionally minimal and cross-platform safe.
const fs = require('fs'); // npm/pnpm/yarn already create shims from package.json "bin" fields.
const path = require('path'); // We avoid attempting to create symlinks or relying on platform-specific tools like `which`/`where`.
const primaryCommand = 'kode'; function postinstallNotice() {
const alternativeCommands = ['kwa', 'kd']; // Only print informational hints; never fail install.
function commandExists(cmd) {
try { try {
execSync(`which ${cmd}`, { stdio: 'ignore' }); console.log('✅ @shareai-lab/kode installed. Commands available: kode, kwa, kd');
return true; console.log(' If shell cannot find them, try reloading your terminal or reinstall globally:');
} catch { console.log(' npm i -g @shareai-lab/kode (or use: npx @shareai-lab/kode)');
return false; } catch {}
}
} }
function setupCommand() {
// Check if primary command exists
if (!commandExists(primaryCommand)) {
console.log(`✅ '${primaryCommand}' command is available and has been set up.`);
return;
}
console.log(`⚠️ '${primaryCommand}' command already exists on your system.`);
// Find an available alternative
for (const alt of alternativeCommands) {
if (!commandExists(alt)) {
// Create alternative command
const binPath = path.join(__dirname, '..', 'cli.js');
const altBinPath = path.join(__dirname, '..', '..', '..', '.bin', alt);
try {
fs.symlinkSync(binPath, altBinPath);
console.log(`✅ Created alternative command '${alt}' instead.`);
console.log(` You can run the tool using: ${alt}`);
return;
} catch (err) {
// Continue to next alternative
}
}
}
console.log(`
All common command names are taken. You can still run the tool using:
- npx @shareai-lab/kode
- Or create your own alias: alias myai='npx @shareai-lab/kode'
`);
}
// Only run in postinstall, not in development
if (process.env.npm_lifecycle_event === 'postinstall') { if (process.env.npm_lifecycle_event === 'postinstall') {
setupCommand(); postinstallNotice();
} }

View File

@ -462,7 +462,16 @@ async function openInEditor(filePath: string): Promise<void> {
const projectDir = process.cwd() const projectDir = process.cwd()
const homeDir = os.homedir() const homeDir = os.homedir()
if (!resolvedPath.startsWith(projectDir) && !resolvedPath.startsWith(homeDir)) { const isSub = (base: string, target: string) => {
const path = require('path')
const rel = path.relative(path.resolve(base), path.resolve(target))
if (!rel || rel === '') return true
if (rel.startsWith('..')) return false
if (path.isAbsolute(rel)) return false
return true
}
if (!isSub(projectDir, resolvedPath) && !isSub(homeDir, resolvedPath)) {
throw new Error('Access denied: File path outside allowed directories') throw new Error('Access denied: File path outside allowed directories')
} }

View File

@ -7,7 +7,7 @@ export function AsciiLogo(): React.ReactNode {
const theme = getTheme() const theme = getTheme()
return ( return (
<Box flexDirection="column" alignItems="flex-start"> <Box flexDirection="column" alignItems="flex-start">
<Text color={theme.claude}>{ASCII_LOGO}</Text> <Text color={theme.kode}>{ASCII_LOGO}</Text>
</Box> </Box>
) )
} }

View File

@ -45,13 +45,13 @@ export function SelectOption({
paddingRight: 1, paddingRight: 1,
}), }),
focusIndicator: () => ({ focusIndicator: () => ({
color: appTheme.claude, color: appTheme.kode,
}), }),
label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({ label: ({ isFocused, isSelected }: { isFocused: boolean; isSelected: boolean }) => ({
color: isSelected color: isSelected
? appTheme.success ? appTheme.success
: isFocused : isFocused
? appTheme.claude ? appTheme.kode
: appTheme.text, : appTheme.text,
bold: isSelected, bold: isSelected,
}), }),

View File

@ -66,7 +66,7 @@ export function Help({
return ( return (
<Box flexDirection="column" padding={1}> <Box flexDirection="column" padding={1}>
<Text bold color={theme.claude}> <Text bold color={theme.kode}>
{`${PRODUCT_NAME} v${MACRO.VERSION}`} {`${PRODUCT_NAME} v${MACRO.VERSION}`}
</Text> </Text>
@ -150,7 +150,7 @@ export function Help({
<Box flexDirection="column"> <Box flexDirection="column">
{customCommands.map((cmd, i) => ( {customCommands.map((cmd, i) => (
<Box key={i} marginLeft={1}> <Box key={i} marginLeft={1}>
<Text bold color={theme.claude}>{`/${cmd.name}`}</Text> <Text bold color={theme.kode}>{`/${cmd.name}`}</Text>
<Text> - {cmd.description}</Text> <Text> - {cmd.description}</Text>
{cmd.aliases && cmd.aliases.length > 0 && ( {cmd.aliases && cmd.aliases.length > 0 && (
<Text color={theme.secondaryText}> <Text color={theme.secondaryText}>

View File

@ -13,9 +13,13 @@ export const MIN_LOGO_WIDTH = 50
export function Logo({ export function Logo({
mcpClients, mcpClients,
isDefaultModel = false, isDefaultModel = false,
updateBannerVersion,
updateBannerCommands,
}: { }: {
mcpClients: WrappedClient[] mcpClients: WrappedClient[]
isDefaultModel?: boolean isDefaultModel?: boolean
updateBannerVersion?: string | null
updateBannerCommands?: string[] | null
}): React.ReactNode { }): React.ReactNode {
const width = Math.max(MIN_LOGO_WIDTH, getCwd().length + 12) const width = Math.max(MIN_LOGO_WIDTH, getCwd().length + 12)
const theme = getTheme() const theme = getTheme()
@ -35,15 +39,36 @@ export function Logo({
return ( return (
<Box flexDirection="column"> <Box flexDirection="column">
<Box <Box
borderColor={theme.claude} borderColor={theme.kode}
borderStyle="round" borderStyle="round"
flexDirection="column" flexDirection="column"
gap={1} gap={1}
paddingLeft={1} paddingLeft={1}
marginRight={2}
width={width} width={width}
> >
{updateBannerVersion ? (
<Box flexDirection="column">
<Text color="yellow">New version available: {updateBannerVersion}</Text>
<Text>Run the following command to update:</Text>
<Text> <Text>
<Text color={theme.claude}></Text> Welcome to{' '} {' '}
{updateBannerCommands?.[0] ?? 'bun add -g @shareai-lab/kode@latest'}
</Text>
<Text>Or:</Text>
<Text>
{' '}
{updateBannerCommands?.[1] ?? 'npm install -g @shareai-lab/kode@latest'}
</Text>
{process.platform !== 'win32' && (
<Text dimColor>
Note: you may need to prefix with "sudo" on macOS/Linux.
</Text>
)}
</Box>
) : null}
<Text>
<Text color={theme.kode}></Text> Welcome to{' '}
<Text bold>{PRODUCT_NAME}</Text> <Text>research preview!</Text> <Text bold>{PRODUCT_NAME}</Text> <Text>research preview!</Text>
</Text> </Text>
{/* <AsciiLogo /> */} {/* <AsciiLogo /> */}

View File

@ -44,7 +44,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
<Box key={pointer} flexDirection="column" marginBottom={1}> <Box key={pointer} flexDirection="column" marginBottom={1}>
<Text> <Text>
🎯{' '} 🎯{' '}
<Text bold color={theme.claude}> <Text bold color={theme.kode}>
{pointer.toUpperCase()} {pointer.toUpperCase()}
</Text>{' '} </Text>{' '}
{model.name} {model.name}
@ -76,7 +76,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
<Box key={pointer} flexDirection="column" marginBottom={1}> <Box key={pointer} flexDirection="column" marginBottom={1}>
<Text> <Text>
🎯{' '} 🎯{' '}
<Text bold color={theme.claude}> <Text bold color={theme.kode}>
{pointer.toUpperCase()} {pointer.toUpperCase()}
</Text>{' '} </Text>{' '}
<Text color={theme.error}> Not configured</Text> <Text color={theme.error}> Not configured</Text>
@ -89,7 +89,7 @@ export function ModelStatusDisplay({ onClose }: Props): React.ReactNode {
<Box key={pointer} flexDirection="column" marginBottom={1}> <Box key={pointer} flexDirection="column" marginBottom={1}>
<Text> <Text>
🎯{' '} 🎯{' '}
<Text bold color={theme.claude}> <Text bold color={theme.kode}>
{pointer.toUpperCase()} {pointer.toUpperCase()}
</Text>{' '} </Text>{' '}
{' '} {' '}

View File

@ -260,13 +260,13 @@ export function WelcomeBox(): React.ReactNode {
const theme = getTheme() const theme = getTheme()
return ( return (
<Box <Box
borderColor={theme.claude} borderColor={theme.kode}
borderStyle="round" borderStyle="round"
paddingX={1} paddingX={1}
width={MIN_LOGO_WIDTH} width={MIN_LOGO_WIDTH}
> >
<Text> <Text>
<Text color={theme.claude}></Text> Welcome to{' '} <Text color={theme.kode}></Text> Welcome to{' '}
<Text bold>{PRODUCT_NAME}</Text> research preview! <Text bold>{PRODUCT_NAME}</Text> research preview!
</Text> </Text>
</Box> </Box>

View File

@ -596,7 +596,7 @@ function PromptInput({
mode === 'bash' mode === 'bash'
? theme.bashBorder ? theme.bashBorder
: mode === 'koding' : mode === 'koding'
? theme.koding ? theme.noting
: theme.secondaryBorder : theme.secondaryBorder
} }
borderDimColor borderDimColor
@ -614,7 +614,7 @@ function PromptInput({
{mode === 'bash' ? ( {mode === 'bash' ? (
<Text color={theme.bashBorder}>&nbsp;!&nbsp;</Text> <Text color={theme.bashBorder}>&nbsp;!&nbsp;</Text>
) : mode === 'koding' ? ( ) : mode === 'koding' ? (
<Text color={theme.koding}>&nbsp;#&nbsp;</Text> <Text color={theme.noting}>&nbsp;#&nbsp;</Text>
) : ( ) : (
<Text color={isLoading ? theme.secondaryText : undefined}> <Text color={isLoading ? theme.secondaryText : undefined}>
&nbsp;&gt;&nbsp; &nbsp;&gt;&nbsp;
@ -668,7 +668,7 @@ function PromptInput({
! for bash mode ! for bash mode
</Text> </Text>
<Text <Text
color={mode === 'koding' ? theme.koding : undefined} color={mode === 'koding' ? theme.noting : undefined}
dimColor={mode !== 'koding'} dimColor={mode !== 'koding'}
> >
· # for AGENTS.md · # for AGENTS.md

View File

@ -96,9 +96,9 @@ export function Spinner(): React.ReactNode {
return ( return (
<Box flexDirection="row" marginTop={1}> <Box flexDirection="row" marginTop={1}>
<Box flexWrap="nowrap" height={1} width={2}> <Box flexWrap="nowrap" height={1} width={2}>
<Text color={getTheme().claude}>{frames[frame]}</Text> <Text color={getTheme().kode}>{frames[frame]}</Text>
</Box> </Box>
<Text color={getTheme().claude}>{message.current} </Text> <Text color={getTheme().kode}>{message.current} </Text>
<Text color={getTheme().secondaryText}> <Text color={getTheme().secondaryText}>
({elapsedTime}s · <Text bold>esc</Text> to interrupt) ({elapsedTime}s · <Text bold>esc</Text> to interrupt)
</Text> </Text>
@ -123,7 +123,7 @@ export function SimpleSpinner(): React.ReactNode {
return ( return (
<Box flexWrap="nowrap" height={1} width={2}> <Box flexWrap="nowrap" height={1} width={2}>
<Text color={getTheme().claude}>{frames[frame]}</Text> <Text color={getTheme().kode}>{frames[frame]}</Text>
</Box> </Box>
) )
} }

View File

@ -14,7 +14,7 @@ export function TaskProgressMessage({ agentType, status, toolCount }: Props) {
return ( return (
<Box flexDirection="column" marginTop={1}> <Box flexDirection="column" marginTop={1}>
<Box flexDirection="row"> <Box flexDirection="row">
<Text color={theme.claude}> </Text> <Text color={theme.kode}> </Text>
<Text color={theme.text} bold> <Text color={theme.text} bold>
[{agentType}] [{agentType}]
</Text> </Text>

View File

@ -20,7 +20,7 @@ export function UserKodingInputMessage({
return ( return (
<Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%"> <Box flexDirection="column" marginTop={addMargin ? 1 : 0} width="100%">
<Box> <Box>
<Text color={getTheme().koding}>#</Text> <Text color={getTheme().noting}>#</Text>
<Text color={getTheme().secondaryText}> {input}</Text> <Text color={getTheme().secondaryText}> {input}</Text>
</Box> </Box>
</Box> </Box>

View File

@ -1,8 +1,25 @@
#!/usr/bin/env -S node --no-warnings=ExperimentalWarning --enable-source-maps #!/usr/bin/env -S node --no-warnings=ExperimentalWarning --enable-source-maps
import { fileURLToPath } from 'node:url'
import { dirname, join } from 'node:path'
import { initSentry } from '../services/sentry' import { initSentry } from '../services/sentry'
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product' import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
initSentry() // Initialize Sentry as early as possible initSentry() // Initialize Sentry as early as possible
// Ensure YOGA_WASM_PATH is set for Ink across run modes (wrapper/dev)
// Resolve yoga.wasm relative to this file when missing using ESM-friendly APIs
try {
if (!process.env.YOGA_WASM_PATH) {
const { existsSync: fsExistsSync } = require('fs')
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const devCandidate = join(__dirname, '../../yoga.wasm')
// Prefer dev path; wrapper already sets env for normal runs
process.env.YOGA_WASM_PATH = fsExistsSync(devCandidate)
? devCandidate
: process.env.YOGA_WASM_PATH
}
} catch {}
// XXX: Without this line (and the Object.keys, even though it seems like it does nothing!), // XXX: Without this line (and the Object.keys, even though it seems like it does nothing!),
// there is a bug in Bun only on Win32 that causes this import to be removed, even though // there is a bug in Bun only on Win32 that causes this import to be removed, even though
// its use is solely because of its side-effects. // its use is solely because of its side-effects.
@ -74,8 +91,11 @@ import {
getLatestVersion, getLatestVersion,
installGlobalPackage, installGlobalPackage,
assertMinVersion, assertMinVersion,
getUpdateCommandSuggestions,
} from '../utils/autoUpdater' } from '../utils/autoUpdater'
import { gt } from 'semver'
import { CACHE_PATHS } from '../utils/log' import { CACHE_PATHS } from '../utils/log'
// import { checkAndNotifyUpdate } from '../utils/autoUpdater'
import { PersistentShell } from '../utils/PersistentShell' import { PersistentShell } from '../utils/PersistentShell'
import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas' import { GATE_USE_EXTERNAL_UPDATER } from '../constants/betas'
import { clearTerminal } from '../utils/terminal' import { clearTerminal } from '../utils/terminal'
@ -290,6 +310,8 @@ async function main() {
} }
} }
// Disabled background notifier to avoid mid-screen logs during REPL
let inputPrompt = '' let inputPrompt = ''
let renderContext: RenderOptions | undefined = { let renderContext: RenderOptions | undefined = {
exitOnCtrlC: false, exitOnCtrlC: false,
@ -417,6 +439,18 @@ ${commandList}`,
} else { } else {
const isDefaultModel = await isDefaultSlowAndCapableModel() const isDefaultModel = await isDefaultSlowAndCapableModel()
// Prefetch update info before first render to place banner at top
const updateInfo = await (async () => {
try {
const latest = await getLatestVersion()
if (latest && gt(latest, MACRO.VERSION)) {
const cmds = await getUpdateCommandSuggestions()
return { version: latest as string, commands: cmds as string[] }
}
} catch {}
return { version: null as string | null, commands: null as string[] | null }
})()
render( render(
<REPL <REPL
commands={commands} commands={commands}
@ -429,6 +463,8 @@ ${commandList}`,
safeMode={safe} safeMode={safe}
mcpClients={mcpClients} mcpClients={mcpClients}
isDefaultModel={isDefaultModel} isDefaultModel={isDefaultModel}
initialUpdateVersion={updateInfo.version}
initialUpdateCommands={updateInfo.commands}
/>, />,
renderContext, renderContext,
) )
@ -1098,11 +1134,11 @@ ${commandList}`,
<Box <Box
flexDirection="column" flexDirection="column"
borderStyle="round" borderStyle="round"
borderColor={theme.claude} borderColor={theme.kode}
padding={1} padding={1}
width={'100%'} width={'100%'}
> >
<Text bold color={theme.claude}> <Text bold color={theme.kode}>
Import MCP Servers from Claude Desktop Import MCP Servers from Claude Desktop
</Text> </Text>
@ -1219,16 +1255,8 @@ ${commandList}`,
// claude update // claude update
program program
.command('update') .command('update')
.description('Check for updates and install if available') .description('Show manual upgrade commands (no auto-install)')
.action(async () => { .action(async () => {
const useExternalUpdater = await checkGate(GATE_USE_EXTERNAL_UPDATER)
if (useExternalUpdater) {
// The external updater intercepts calls to "claude update", which means if we have received
// this command at all, the extenral updater isn't installed on this machine.
console.log(`This version of ${PRODUCT_NAME} is no longer supported.`)
process.exit(0)
}
logEvent('tengu_update_check', {}) logEvent('tengu_update_check', {})
console.log(`Current version: ${MACRO.VERSION}`) console.log(`Current version: ${MACRO.VERSION}`)
console.log('Checking for updates...') console.log('Checking for updates...')
@ -1246,30 +1274,12 @@ ${commandList}`,
} }
console.log(`New version available: ${latestVersion}`) console.log(`New version available: ${latestVersion}`)
console.log('Installing update...') const { getUpdateCommandSuggestions } = await import('../utils/autoUpdater')
const cmds = await getUpdateCommandSuggestions()
const status = await installGlobalPackage() console.log('\nRun one of the following commands to update:')
for (const c of cmds) console.log(` ${c}`)
switch (status) { if (process.platform !== 'win32') {
case 'success': console.log('\nNote: you may need to prefix with "sudo" on macOS/Linux.')
console.log(`Successfully updated to version ${latestVersion}`)
break
case 'no_permissions':
console.error('Error: Insufficient permissions to install update')
console.error('Try running with sudo or fix npm permissions')
process.exit(1)
break
case 'install_failed':
console.error('Error: Failed to install update')
process.exit(1)
break
case 'in_progress':
console.error(
'Error: Another instance is currently performing an update',
)
console.error('Please wait and try again later')
process.exit(1)
break
} }
process.exit(0) process.exit(0)
}) })
@ -1501,9 +1511,23 @@ process.on('exit', () => {
PersistentShell.getInstance().close() PersistentShell.getInstance().close()
}) })
process.on('SIGINT', () => { function gracefulExit(code = 0) {
console.log('SIGINT') try { resetCursor() } catch {}
process.exit(0) try { PersistentShell.getInstance().close() } catch {}
process.exit(code)
}
process.on('SIGINT', () => gracefulExit(0))
process.on('SIGTERM', () => gracefulExit(0))
// Windows CTRL+BREAK
process.on('SIGBREAK', () => gracefulExit(0))
process.on('unhandledRejection', err => {
console.error('Unhandled rejection:', err)
gracefulExit(1)
})
process.on('uncaughtException', err => {
console.error('Uncaught exception:', err)
gracefulExit(1)
}) })
function resetCursor() { function resetCursor() {

View File

@ -44,6 +44,7 @@ import type { WrappedClient } from '../services/mcpClient'
import type { Tool } from '../Tool' import type { Tool } from '../Tool'
import { AutoUpdaterResult } from '../utils/autoUpdater' import { AutoUpdaterResult } from '../utils/autoUpdater'
import { getGlobalConfig, saveGlobalConfig } from '../utils/config' import { getGlobalConfig, saveGlobalConfig } from '../utils/config'
import { MACRO } from '../constants/macros'
import { logEvent } from '../services/statsig' import { logEvent } from '../services/statsig'
import { getNextAvailableLogForkNumber } from '../utils/log' import { getNextAvailableLogForkNumber } from '../utils/log'
import { import {
@ -87,6 +88,9 @@ type Props = {
mcpClients?: WrappedClient[] mcpClients?: WrappedClient[]
// Flag to indicate if current model is default // Flag to indicate if current model is default
isDefaultModel?: boolean isDefaultModel?: boolean
// Update banner info passed from CLI before first render
initialUpdateVersion?: string | null
initialUpdateCommands?: string[] | null
} }
export type BinaryFeedbackContext = { export type BinaryFeedbackContext = {
@ -108,6 +112,8 @@ export function REPL({
initialMessages, initialMessages,
mcpClients = [], mcpClients = [],
isDefaultModel = true, isDefaultModel = true,
initialUpdateVersion,
initialUpdateCommands,
}: Props): React.ReactNode { }: Props): React.ReactNode {
// TODO: probably shouldn't re-read config from file synchronously on every keystroke // TODO: probably shouldn't re-read config from file synchronously on every keystroke
const verbose = verboseFromCLI ?? getGlobalConfig().verbose const verbose = verboseFromCLI ?? getGlobalConfig().verbose
@ -149,6 +155,10 @@ export function REPL({
const [binaryFeedbackContext, setBinaryFeedbackContext] = const [binaryFeedbackContext, setBinaryFeedbackContext] =
useState<BinaryFeedbackContext | null>(null) useState<BinaryFeedbackContext | null>(null)
// New version banner: passed in from CLI to guarantee top placement
const updateAvailableVersion = initialUpdateVersion ?? null
const updateCommands = initialUpdateCommands ?? null
// No separate Static for banner; it renders inside Logo
const getBinaryFeedbackResponse = useCallback( const getBinaryFeedbackResponse = useCallback(
( (
@ -209,6 +219,8 @@ export function REPL({
} }
}, [messages, showCostDialog, haveShownCostDialog]) }, [messages, showCostDialog, haveShownCostDialog])
// Update banner is provided by CLI at startup; no async check here.
const canUseTool = useCanUseTool(setToolUseConfirm) const canUseTool = useCanUseTool(setToolUseConfirm)
async function onInit() { async function onInit() {
@ -478,7 +490,12 @@ export function REPL({
type: 'static', type: 'static',
jsx: ( jsx: (
<Box flexDirection="column" key={`logo${forkNumber}`}> <Box flexDirection="column" key={`logo${forkNumber}`}>
<Logo mcpClients={mcpClients} isDefaultModel={isDefaultModel} /> <Logo
mcpClients={mcpClients}
isDefaultModel={isDefaultModel}
updateBannerVersion={updateAvailableVersion}
updateBannerCommands={updateCommands}
/>
<ProjectOnboarding workspaceDir={getOriginalCwd()} /> <ProjectOnboarding workspaceDir={getOriginalCwd()} />
</Box> </Box>
), ),
@ -602,6 +619,7 @@ export function REPL({
return ( return (
<PermissionProvider isBypassPermissionsModeAvailable={!safeMode}> <PermissionProvider isBypassPermissionsModeAvailable={!safeMode}>
{/* Update banner now renders inside Logo for stable placement */}
<ModeIndicator /> <ModeIndicator />
<React.Fragment key={`static-messages-${forkNumber}`}> <React.Fragment key={`static-messages-${forkNumber}`}>
<Static <Static

View File

@ -518,7 +518,7 @@ export async function getCompletionWithProfile(
messageCount: opts.messages?.length || 0, messageCount: opts.messages?.length || 0,
streamMode: opts.stream, streamMode: opts.stream,
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
modelProfileName: modelProfile?.modelName, modelProfileModelName: modelProfile?.modelName,
modelProfileName: modelProfile?.name, modelProfileName: modelProfile?.name,
}) })

View File

@ -107,7 +107,8 @@ export function logEvent(
} }
export const checkGate = memoize(async (gateName: string): Promise<boolean> => { export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
return true // Default to disabled gates when Statsig is not active
return false
// if (env.isCI || process.env.NODE_ENV === 'test') { // if (env.isCI || process.env.NODE_ENV === 'test') {
// return false // return false
// } // }
@ -120,7 +121,7 @@ export const checkGate = memoize(async (gateName: string): Promise<boolean> => {
}) })
export const useStatsigGate = (gateName: string, defaultValue = false) => { export const useStatsigGate = (gateName: string, defaultValue = false) => {
return true return false
// const [gateValue, setGateValue] = React.useState(defaultValue) // const [gateValue, setGateValue] = React.useState(defaultValue)
// React.useEffect(() => { // React.useEffect(() => {
// checkGate(gateName).then(setGateValue) // checkGate(gateName).then(setGateValue)

View File

@ -263,22 +263,16 @@ export async function startAgentWatcher(onChange?: () => void): Promise<void> {
* Stop watching agent configuration directories * Stop watching agent configuration directories
*/ */
export async function stopAgentWatcher(): Promise<void> { export async function stopAgentWatcher(): Promise<void> {
const closePromises = watchers.map(watcher => // FSWatcher.close() is synchronous and does not accept a callback on Node 18/20
new Promise<void>((resolve) => {
try { try {
watcher.close((err) => { for (const watcher of watchers) {
if (err) { try {
watcher.close()
} catch (err) {
console.error('Failed to close file watcher:', err) console.error('Failed to close file watcher:', err)
} }
resolve()
})
} catch (error) {
console.error('Error closing watcher:', error)
resolve()
} }
}) } finally {
)
await Promise.allSettled(closePromises)
watchers = [] watchers = []
} }
}

View File

@ -12,13 +12,16 @@ import {
} from 'fs' } from 'fs'
import { platform } from 'process' import { platform } from 'process'
import { execFileNoThrow } from './execFileNoThrow' import { execFileNoThrow } from './execFileNoThrow'
import { spawn } from 'child_process'
import { logError } from './log' import { logError } from './log'
import { accessSync } from 'fs' import { accessSync } from 'fs'
import { CLAUDE_BASE_DIR } from './env' import { CLAUDE_BASE_DIR } from './env'
import { logEvent, getDynamicConfig } from '../services/statsig' import { logEvent, getDynamicConfig } from '../services/statsig'
import { lt } from 'semver' import { lt, gt } from 'semver'
import { MACRO } from '../constants/macros' import { MACRO } from '../constants/macros'
import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product' import { PRODUCT_COMMAND, PRODUCT_NAME } from '../constants/product'
import { getGlobalConfig, saveGlobalConfig, isAutoUpdaterDisabled } from './config'
import { env } from './env'
export type InstallStatus = export type InstallStatus =
| 'success' | 'success'
| 'no_permissions' | 'no_permissions'
@ -49,14 +52,12 @@ export async function assertMinVersion(): Promise<void> {
versionConfig.minVersion && versionConfig.minVersion &&
lt(MACRO.VERSION, versionConfig.minVersion) lt(MACRO.VERSION, versionConfig.minVersion)
) { ) {
const suggestions = await getUpdateCommandSuggestions()
const cmdLines = suggestions.map(c => ` ${c}`).join('\n')
console.error(` console.error(`
It looks like your version of ${PRODUCT_NAME} (${MACRO.VERSION}) needs an update. ${PRODUCT_NAME} (${MACRO.VERSION}) ${versionConfig.minVersion}
A newer version (${versionConfig.minVersion} or higher) is required to continue.
${cmdLines}
To update, please run:
${PRODUCT_COMMAND} update
This will ensure you have access to the latest features and improvements.
`) `)
process.exit(1) process.exit(1)
} }
@ -267,21 +268,48 @@ export function getPermissionsCommand(npmPrefix: string): string {
} }
export async function getLatestVersion(): Promise<string | null> { export async function getLatestVersion(): Promise<string | null> {
// 1) Try npm CLI (fast when available)
try {
const abortController = new AbortController() const abortController = new AbortController()
setTimeout(() => abortController.abort(), 5000) setTimeout(() => abortController.abort(), 5000)
const result = await execFileNoThrow( const result = await execFileNoThrow(
'npm', 'npm',
['view', MACRO.PACKAGE_URL, 'version'], ['view', MACRO.PACKAGE_URL, 'version'],
abortController.signal, abortController.signal,
) )
if (result.code !== 0) { if (result.code === 0) {
const v = result.stdout.trim()
if (v) return v
}
} catch {}
// 2) Fallback: fetch npm registry (works in Bun/Node without npm)
try {
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 5000)
const res = await fetch(
`https://registry.npmjs.org/${encodeURIComponent(MACRO.PACKAGE_URL)}`,
{
method: 'GET',
headers: {
Accept: 'application/vnd.npm.install-v1+json',
'User-Agent': `${PRODUCT_NAME}/${MACRO.VERSION}`,
},
signal: controller.signal,
},
)
clearTimeout(timer)
if (!res.ok) return null
const json: any = await res.json().catch(() => null)
const latest = json && json['dist-tags'] && json['dist-tags'].latest
return typeof latest === 'string' ? latest : null
} catch {
return null return null
} }
return result.stdout.trim()
} }
export async function installGlobalPackage(): Promise<InstallStatus> { export async function installGlobalPackage(): Promise<InstallStatus> {
// Detect preferred package manager and install accordingly
if (!acquireLock()) { if (!acquireLock()) {
logError('Another process is currently installing an update') logError('Another process is currently installing an update')
// Log the lock contention to statsig // Log the lock contention to statsig
@ -293,26 +321,138 @@ export async function installGlobalPackage(): Promise<InstallStatus> {
} }
try { try {
const manager = await detectPackageManager()
if (manager === 'npm') {
const { hasPermissions } = await checkNpmPermissions() const { hasPermissions } = await checkNpmPermissions()
if (!hasPermissions) { if (!hasPermissions) {
return 'no_permissions' return 'no_permissions'
} }
// Stream实时输出减少用户等待感
const installResult = await execFileNoThrow('npm', [ const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
'install', if (code !== 0) {
'-g', logError(`Failed to install new version via npm (exit ${code})`)
MACRO.PACKAGE_URL,
])
if (installResult.code !== 0) {
logError(
`Failed to install new version of claude: ${installResult.stdout} ${installResult.stderr}`,
)
return 'install_failed' return 'install_failed'
} }
return 'success'
}
if (manager === 'bun') {
const code = await runStreaming('bun', ['add', '-g', `${MACRO.PACKAGE_URL}@latest`])
if (code !== 0) {
logError(`Failed to install new version via bun (exit ${code})`)
return 'install_failed'
}
return 'success'
}
// Fallback to npm if unknown
const { hasPermissions } = await checkNpmPermissions()
if (!hasPermissions) return 'no_permissions'
const code = await runStreaming('npm', ['install', '-g', MACRO.PACKAGE_URL])
if (code !== 0) return 'install_failed'
return 'success' return 'success'
} finally { } finally {
// Ensure we always release the lock // Ensure we always release the lock
releaseLock() releaseLock()
} }
} }
export type PackageManager = 'npm' | 'bun'
export async function detectPackageManager(): Promise<PackageManager> {
// Respect explicit override if provided later via config/env (future-proof)
try {
// Heuristic 1: npm available and global root resolvable
const npmRoot = await execFileNoThrow('npm', ['-g', 'root'])
if (npmRoot.code === 0 && npmRoot.stdout.trim()) {
return 'npm'
}
} catch {}
try {
// Heuristic 2: running on a system with bun and installed path hints bun
const bunVer = await execFileNoThrow('bun', ['--version'])
if (bunVer.code === 0) {
// BUN_INSTALL defaults to ~/.bun; if our package lives under that tree, prefer bun
// If npm not detected but bun is available, choose bun
return 'bun'
}
} catch {}
// Default to npm when uncertain
return 'npm'
}
function runStreaming(cmd: string, args: string[]): Promise<number> {
return new Promise(resolve => {
// 打印正在使用的包管理器与命令,增强透明度
try {
// eslint-disable-next-line no-console
console.log(`> ${cmd} ${args.join(' ')}`)
} catch {}
const child = spawn(cmd, args, {
stdio: 'inherit',
env: process.env,
})
child.on('close', code => resolve(code ?? 0))
child.on('error', () => resolve(1))
})
}
/**
* Generate human-friendly update commands for the detected package manager.
* Also includes an alternative manager command as fallback for users.
*/
export async function getUpdateCommandSuggestions(): Promise<string[]> {
// Prefer Bun first, then npm (consistent, simple UX). Include @latest.
return [
`bun add -g ${MACRO.PACKAGE_URL}@latest`,
`npm install -g ${MACRO.PACKAGE_URL}@latest`,
]
}
/**
* Non-blocking update notifier (daily)
* - Respects CI and disabled auto-updater
* - Uses env.hasInternetAccess() to quickly skip offline cases
* - Stores last check timestamp + last suggested version in global config
*/
export async function checkAndNotifyUpdate(): Promise<void> {
try {
if (process.env.NODE_ENV === 'test') return
if (await isAutoUpdaterDisabled()) return
if (await env.getIsDocker()) return
if (!(await env.hasInternetAccess())) return
const config: any = getGlobalConfig()
const now = Date.now()
const DAY_MS = 24 * 60 * 60 * 1000
const lastCheck = Number(config.lastUpdateCheckAt || 0)
if (lastCheck && now - lastCheck < DAY_MS) return
const latest = await getLatestVersion()
if (!latest) {
// Still record the check to avoid spamming
saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
return
}
if (gt(latest, MACRO.VERSION)) {
// Update stored state and print a low-noise hint
saveGlobalConfig({
...config,
lastUpdateCheckAt: now,
lastSuggestedVersion: latest,
})
const suggestions = await getUpdateCommandSuggestions()
const first = suggestions[0]
console.log(`New version available: ${latest}. Recommended: ${first}`)
} else {
saveGlobalConfig({ ...config, lastUpdateCheckAt: now })
}
} catch (error) {
// Never block or throw; just log and move on
logError(`update-notify: ${error}`)
}
}

View File

@ -173,6 +173,8 @@ export type GlobalConfig = {
modelProfiles?: ModelProfile[] // Model configuration list modelProfiles?: ModelProfile[] // Model configuration list
modelPointers?: ModelPointers // Model pointer system modelPointers?: ModelPointers // Model pointer system
defaultModelName?: string // Default model defaultModelName?: string // Default model
// Update notifications
lastDismissedUpdateVersion?: string
} }
export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
@ -196,6 +198,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
reasoning: '', reasoning: '',
quick: '', quick: '',
}, },
lastDismissedUpdateVersion: undefined,
} }
export const GLOBAL_CONFIG_KEYS = [ export const GLOBAL_CONFIG_KEYS = [

View File

@ -98,12 +98,15 @@ export function isInDirectory(
: normalizedCwd + sep : normalizedCwd + sep
// Join with a base directory to make them absolute-like for comparison // Join with a base directory to make them absolute-like for comparison
// Using 'dummy' as base to avoid any actual file system dependencies
const fullPath = resolvePath(cwd(), normalizedCwd, normalizedPath) const fullPath = resolvePath(cwd(), normalizedCwd, normalizedPath)
const fullCwd = resolvePath(cwd(), normalizedCwd) const fullCwd = resolvePath(cwd(), normalizedCwd)
// Check if the path starts with the cwd // Robust subpath check using path.relative (case-insensitive on Windows)
return fullPath.startsWith(fullCwd) const rel = relative(fullCwd, fullPath)
if (!rel || rel === '') return true
if (rel.startsWith('..')) return false
if (isAbsolute(rel)) return false
return true
} }
export function readTextContent( export function readTextContent(

View File

@ -1,4 +1,4 @@
import { isAbsolute, resolve } from 'path' import { isAbsolute, resolve, relative, sep } from 'path'
import { getCwd, getOriginalCwd } from '../state' import { getCwd, getOriginalCwd } from '../state'
// In-memory storage for file permissions that resets each session // In-memory storage for file permissions that resets each session
@ -12,7 +12,26 @@ const writeFileAllowedDirectories: Set<string> = new Set()
* @returns Absolute path * @returns Absolute path
*/ */
export function toAbsolutePath(path: string): string { export function toAbsolutePath(path: string): string {
return isAbsolute(path) ? resolve(path) : resolve(getCwd(), path) const abs = isAbsolute(path) ? resolve(path) : resolve(getCwd(), path)
return normalizeForCompare(abs)
}
function normalizeForCompare(p: string): string {
// Normalize separators and resolve .. and . segments
const norm = resolve(p)
// On Windows, comparisons should be case-insensitive
return process.platform === 'win32' ? norm.toLowerCase() : norm
}
function isSubpath(base: string, target: string): boolean {
const rel = relative(base, target)
// If different drive letters on Windows, relative returns the target path
if (!rel || rel === '') return true
// Not a subpath if it goes up to parent
if (rel.startsWith('..')) return false
// Not a subpath if absolute
if (isAbsolute(rel)) return false
return true
} }
/** /**
@ -22,7 +41,8 @@ export function toAbsolutePath(path: string): string {
*/ */
export function pathInOriginalCwd(path: string): boolean { export function pathInOriginalCwd(path: string): boolean {
const absolutePath = toAbsolutePath(path) const absolutePath = toAbsolutePath(path)
return absolutePath.startsWith(toAbsolutePath(getOriginalCwd())) const base = toAbsolutePath(getOriginalCwd())
return isSubpath(base, absolutePath)
} }
/** /**
@ -32,12 +52,8 @@ export function pathInOriginalCwd(path: string): boolean {
*/ */
export function hasReadPermission(directory: string): boolean { export function hasReadPermission(directory: string): boolean {
const absolutePath = toAbsolutePath(directory) const absolutePath = toAbsolutePath(directory)
for (const allowedPath of readFileAllowedDirectories) { for (const allowedPath of readFileAllowedDirectories) {
// Permission exists for this directory or a path prefix if (isSubpath(allowedPath, absolutePath)) return true
if (absolutePath.startsWith(allowedPath)) {
return true
}
} }
return false return false
} }
@ -49,12 +65,8 @@ export function hasReadPermission(directory: string): boolean {
*/ */
export function hasWritePermission(directory: string): boolean { export function hasWritePermission(directory: string): boolean {
const absolutePath = toAbsolutePath(directory) const absolutePath = toAbsolutePath(directory)
for (const allowedPath of writeFileAllowedDirectories) { for (const allowedPath of writeFileAllowedDirectories) {
// Permission exists for this directory or a path prefix if (isSubpath(allowedPath, absolutePath)) return true
if (absolutePath.startsWith(allowedPath)) {
return true
}
} }
return false return false
} }
@ -65,10 +77,9 @@ export function hasWritePermission(directory: string): boolean {
*/ */
function saveReadPermission(directory: string): void { function saveReadPermission(directory: string): void {
const absolutePath = toAbsolutePath(directory) const absolutePath = toAbsolutePath(directory)
// Remove any existing subpaths contained by this new path
// Clean up any existing subdirectories of this path for (const allowedPath of Array.from(readFileAllowedDirectories)) {
for (const allowedPath of readFileAllowedDirectories) { if (isSubpath(absolutePath, allowedPath)) {
if (allowedPath.startsWith(absolutePath)) {
readFileAllowedDirectories.delete(allowedPath) readFileAllowedDirectories.delete(allowedPath)
} }
} }
@ -92,10 +103,8 @@ export function grantReadPermissionForOriginalDir(): void {
*/ */
function saveWritePermission(directory: string): void { function saveWritePermission(directory: string): void {
const absolutePath = toAbsolutePath(directory) const absolutePath = toAbsolutePath(directory)
for (const allowedPath of Array.from(writeFileAllowedDirectories)) {
// Clean up any existing subdirectories of this path if (isSubpath(absolutePath, allowedPath)) {
for (const allowedPath of writeFileAllowedDirectories) {
if (allowedPath.startsWith(absolutePath)) {
writeFileAllowedDirectories.delete(allowedPath) writeFileAllowedDirectories.delete(allowedPath)
} }
} }

View File

@ -1,5 +1,5 @@
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, renameSync } from 'node:fs' import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, unlinkSync, renameSync } from 'node:fs'
import { join, dirname, normalize, resolve, extname } from 'node:path' import { join, dirname, normalize, resolve, extname, relative, isAbsolute } from 'node:path'
import { homedir } from 'node:os' import { homedir } from 'node:os'
/** /**
@ -98,7 +98,12 @@ export class SecureFileService {
// 检查是否在允许的基础路径中 // 检查是否在允许的基础路径中
const isInAllowedPath = Array.from(this.allowedBasePaths).some(basePath => { const isInAllowedPath = Array.from(this.allowedBasePaths).some(basePath => {
return absolutePath.startsWith(basePath) const base = resolve(basePath)
const rel = relative(base, absolutePath)
if (!rel || rel === '') return true
if (rel.startsWith('..')) return false
if (isAbsolute(rel)) return false
return true
}) })
if (!isInAllowedPath) { if (!isInAllowedPath) {

View File

@ -2,18 +2,16 @@ import { getGlobalConfig } from './config'
export interface Theme { export interface Theme {
bashBorder: string bashBorder: string
claude: string kode: string
koding: string noting: string
permission: string permission: string
secondaryBorder: string secondaryBorder: string
text: string text: string
secondaryText: string secondaryText: string
suggestion: string suggestion: string
// Semantic colors
success: string success: string
error: string error: string
warning: string warning: string
// UI colors
primary: string primary: string
secondary: string secondary: string
diff: { diff: {
@ -25,9 +23,9 @@ export interface Theme {
} }
const lightTheme: Theme = { const lightTheme: Theme = {
bashBorder: '#ff0087', bashBorder: '#FF6E57',
claude: '#7aff59ff', kode: '#FFC233',
koding: '#9dff00ff', noting: '#222222',
permission: '#e9c61aff', permission: '#e9c61aff',
secondaryBorder: '#999', secondaryBorder: '#999',
text: '#000', text: '#000',
@ -47,31 +45,31 @@ const lightTheme: Theme = {
} }
const lightDaltonizedTheme: Theme = { const lightDaltonizedTheme: Theme = {
bashBorder: '#0066cc', // Blue instead of pink for better contrast bashBorder: '#FF6E57',
claude: '#5f97cd', // Orange adjusted for deuteranopia kode: '#FFC233',
koding: '#0000ff', noting: '#222222',
permission: '#3366ff', // Brighter blue for better visibility permission: '#3366ff',
secondaryBorder: '#999', secondaryBorder: '#999',
text: '#000', text: '#000',
secondaryText: '#666', secondaryText: '#666',
suggestion: '#3366ff', suggestion: '#3366ff',
success: '#006699', // Blue instead of green success: '#006699',
error: '#cc0000', // Pure red for better distinction error: '#cc0000',
warning: '#ff9900', // Orange adjusted for deuteranopia warning: '#ff9900',
primary: '#000', primary: '#000',
secondary: '#666', secondary: '#666',
diff: { diff: {
added: '#99ccff', // Light blue instead of green added: '#99ccff',
removed: '#ffcccc', // Light red for better contrast removed: '#ffcccc',
addedDimmed: '#d1e7fd', addedDimmed: '#d1e7fd',
removedDimmed: '#ffe9e9', removedDimmed: '#ffe9e9',
}, },
} }
const darkTheme: Theme = { const darkTheme: Theme = {
bashBorder: '#fd5db1', bashBorder: '#FF6E57',
claude: '#5f97cd', kode: '#FFC233',
koding: '#0000ff', noting: '#222222',
permission: '#b1b9f9', permission: '#b1b9f9',
secondaryBorder: '#888', secondaryBorder: '#888',
text: '#fff', text: '#fff',
@ -91,32 +89,28 @@ const darkTheme: Theme = {
} }
const darkDaltonizedTheme: Theme = { const darkDaltonizedTheme: Theme = {
bashBorder: '#3399ff', // Bright blue instead of pink bashBorder: '#FF6E57',
claude: '#5f97cd', // Orange adjusted for deuteranopia kode: '#FFC233',
koding: '#0000ff', noting: '#222222',
permission: '#99ccff', // Light blue for better contrast permission: '#99ccff',
secondaryBorder: '#888', secondaryBorder: '#888',
text: '#fff', text: '#fff',
secondaryText: '#999', secondaryText: '#999',
suggestion: '#99ccff', suggestion: '#99ccff',
success: '#3399ff', // Bright blue instead of green success: '#3399ff',
error: '#ff6666', // Bright red for better visibility error: '#ff6666',
warning: '#ffcc00', // Yellow-orange for deuteranopia warning: '#ffcc00',
primary: '#fff', primary: '#fff',
secondary: '#999', secondary: '#999',
diff: { diff: {
added: '#004466', // Dark blue instead of green added: '#004466',
removed: '#660000', // Dark red for better contrast removed: '#660000',
addedDimmed: '#3e515b', addedDimmed: '#3e515b',
removedDimmed: '#3e2c2c', removedDimmed: '#3e2c2c',
}, },
} }
export type ThemeNames = export type ThemeNames = 'dark' | 'light' | 'light-daltonized' | 'dark-daltonized'
| 'dark'
| 'light'
| 'light-daltonized'
| 'dark-daltonized'
export function getTheme(overrideTheme?: ThemeNames): Theme { export function getTheme(overrideTheme?: ThemeNames): Theme {
const config = getGlobalConfig() const config = getGlobalConfig()