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
110 changes: 80 additions & 30 deletions src/core/config/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getCwd } from '@utils/state'
import { safeParseJSON } from '@utils/text/json'
import { ConfigParseError } from '@utils/text/errors'
import { debug as debugLogger } from '@utils/log/debugLogger'
import { logStartupProfileDuration } from '@utils/config/startupProfile'
import {
DEFAULT_GLOBAL_CONFIG,
DEFAULT_PROJECT_CONFIG,
Expand Down Expand Up @@ -91,7 +92,7 @@ function findMatchingProjectKey(

export function checkHasTrustDialogAccepted(): boolean {
let currentPath = getCwd()
const config = getConfig(getGlobalConfigFilePath(), DEFAULT_GLOBAL_CONFIG)
const config = getLoadedGlobalConfig()

while (true) {
const projectKey = findMatchingProjectKey(config.projects, currentPath)
Expand All @@ -117,6 +118,36 @@ const TEST_PROJECT_CONFIG_FOR_TESTING: ProjectConfig = {
...DEFAULT_PROJECT_CONFIG,
}

let globalConfigCache: GlobalConfig | null = null
let globalConfigCachePath: string | null = null

function cacheGlobalConfig(file: string, config: GlobalConfig): GlobalConfig {
const migrated = migrateModelProfilesRemoveId(config)
globalConfigCachePath = file
globalConfigCache = cloneDeep(migrated)
return cloneDeep(migrated)
}

function getCachedGlobalConfig(file: string): GlobalConfig | null {
if (globalConfigCachePath !== file || !globalConfigCache) return null
return cloneDeep(globalConfigCache)
}

function getLoadedGlobalConfig(throwOnInvalid?: boolean): GlobalConfig {
const file = getGlobalConfigFilePath()
const cached = getCachedGlobalConfig(file)
if (cached) return cached
return cacheGlobalConfig(
file,
getConfig(file, DEFAULT_GLOBAL_CONFIG, throwOnInvalid),
)
}

export function clearConfigCacheForTesting(): void {
globalConfigCache = null
globalConfigCachePath = null
}

export function saveGlobalConfig(config: GlobalConfig): void {
if (process.env.NODE_ENV === 'test') {
for (const key in config) {
Expand All @@ -125,23 +156,24 @@ export function saveGlobalConfig(config: GlobalConfig): void {
return
}

saveConfig(
getGlobalConfigFilePath(),
{
...config,
projects: getConfig(getGlobalConfigFilePath(), DEFAULT_GLOBAL_CONFIG)
.projects,
},
DEFAULT_GLOBAL_CONFIG,
)
const file = getGlobalConfigFilePath()
const existingConfig =
getCachedGlobalConfig(file) ?? getConfig(file, DEFAULT_GLOBAL_CONFIG)
const nextConfig = {
...config,
projects: existingConfig.projects,
}

if (saveConfig(file, nextConfig, DEFAULT_GLOBAL_CONFIG)) {
cacheGlobalConfig(file, nextConfig)
}
}

export function getGlobalConfig(): GlobalConfig {
if (process.env.NODE_ENV === 'test') {
return TEST_GLOBAL_CONFIG_FOR_TESTING
}
const config = getConfig(getGlobalConfigFilePath(), DEFAULT_GLOBAL_CONFIG)
return migrateModelProfilesRemoveId(config)
return getLoadedGlobalConfig()
}

export function normalizeApiKeyForConfig(apiKey: string): string {
Expand All @@ -165,7 +197,8 @@ function saveConfig<A extends object>(
file: string,
config: A,
defaultConfig: A,
): void {
): boolean {
const startedAt = Date.now()
const filteredConfig = Object.fromEntries(
Object.entries(config).filter(
([key, value]) =>
Expand All @@ -174,6 +207,8 @@ function saveConfig<A extends object>(
)
try {
writeFileSync(file, JSON.stringify(filteredConfig, null, 2), 'utf-8')
logStartupProfileDuration('config_write', Date.now() - startedAt, { file })
return true
} catch (error) {
const err = error as NodeJS.ErrnoException
if (
Expand All @@ -185,7 +220,7 @@ function saveConfig<A extends object>(
file,
reason: String(err.code),
})
return
return false
}
throw error
}
Expand All @@ -195,14 +230,15 @@ let configReadingAllowed = false

export function enableConfigs(): void {
configReadingAllowed = true
getConfig(getGlobalConfigFilePath(), DEFAULT_GLOBAL_CONFIG, true)
getLoadedGlobalConfig(true)
}

function getConfig<A>(
file: string,
defaultConfig: A,
throwOnInvalid?: boolean,
): A {
const startedAt = Date.now()
void configReadingAllowed

debugLogger.state('CONFIG_LOAD_START', {
Expand All @@ -217,7 +253,12 @@ function getConfig<A>(
reason: 'file_not_exists',
defaultConfigKeys: Object.keys(defaultConfig as object).join(', '),
})
return cloneDeep(defaultConfig)
const config = cloneDeep(defaultConfig)
logStartupProfileDuration('config_read', Date.now() - startedAt, {
file,
source: 'default',
})
return config
}

try {
Expand Down Expand Up @@ -246,6 +287,10 @@ function getConfig<A>(
finalConfigKeys: Object.keys(finalConfig as object).join(', '),
})

logStartupProfileDuration('config_read', Date.now() - startedAt, {
file,
source: 'file',
})
return finalConfig
} catch (error) {
const errorMessage =
Expand Down Expand Up @@ -278,7 +323,12 @@ function getConfig<A>(
action: 'using_default_config',
})

return cloneDeep(defaultConfig)
const config = cloneDeep(defaultConfig)
logStartupProfileDuration('config_read', Date.now() - startedAt, {
file,
source: 'fallback',
})
return config
}
}

Expand All @@ -288,7 +338,7 @@ export function getCurrentProjectConfig(): ProjectConfig {
}

const absolutePath = resolve(getCwd())
const config = getConfig(getGlobalConfigFilePath(), DEFAULT_GLOBAL_CONFIG)
const config = getLoadedGlobalConfig()

if (!config.projects) {
return defaultConfigForProject(absolutePath)
Expand Down Expand Up @@ -321,22 +371,22 @@ export function saveCurrentProjectConfig(projectConfig: ProjectConfig): void {
}
return
}
const config = getConfig(getGlobalConfigFilePath(), DEFAULT_GLOBAL_CONFIG)
const config = getLoadedGlobalConfig()
const resolvedCwd = resolve(getCwd())
const existingKey = findMatchingProjectKey(config.projects, resolvedCwd)
const storageKey = existingKey ?? resolvedCwd

saveConfig(
getGlobalConfigFilePath(),
{
...config,
projects: {
...config.projects,
[storageKey]: projectConfig,
},
const nextConfig = {
...config,
projects: {
...config.projects,
[storageKey]: projectConfig,
},
DEFAULT_GLOBAL_CONFIG,
)
}

const file = getGlobalConfigFilePath()
if (saveConfig(file, nextConfig, DEFAULT_GLOBAL_CONFIG)) {
cacheGlobalConfig(file, nextConfig)
}
}

export async function isAutoUpdaterDisabled(): Promise<boolean> {
Expand Down
46 changes: 10 additions & 36 deletions src/entrypoints/cli/runCli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ import {
getConfigForCLI,
listConfigForCLI,
enableConfigs,
validateAndRepairAllGPT5Profiles,
} from '@utils/config'
import { cwd } from 'process'
import { dateToFilename, logError, parseLogFilename } from '@utils/log'
Expand Down Expand Up @@ -99,14 +98,6 @@ export async function runCli() {

try {
enableConfigs()

queueMicrotask(() => {
try {
validateAndRepairAllGPT5Profiles()
} catch (repairError) {
logError(`GPT-5 configuration validation failed: ${repairError}`)
}
})
} catch (error: unknown) {
if (error instanceof ConfigParseError) {
await showInvalidConfigDialog({ error })
Expand Down Expand Up @@ -600,34 +591,15 @@ async function parseArgs(
process.exit(1)
}

const updateInfo = await (async () => {
try {
const [
{ getLatestVersion, getUpdateCommandSuggestions },
semverMod,
] = await Promise.all([
import('@utils/session/autoUpdater'),
import('semver'),
])
const semver: any = (semverMod as any)?.default ?? semverMod
const gt = semver?.gt
if (typeof gt !== 'function')
return {
version: null as string | null,
commands: null as string[] | null,
}
// Start update check early, outside REPL render critical path
const updateCheckPromise = import('@utils/session/autoUpdater')
.then(({ getUpdateBannerInfo }) => getUpdateBannerInfo())
.catch(() => ({ version: null, commands: null }))

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,
}
})()
const updateInfo = {
version: null as string | null,
commands: null as string[] | null,
}

if (needsResumeSelector) {
const sessions = listKodeAgentSessions({ cwd })
Expand Down Expand Up @@ -656,6 +628,7 @@ async function parseArgs(
forkSessionId={sessionId ? String(sessionId) : null}
initialUpdateVersion={updateInfo.version}
initialUpdateCommands={updateInfo.commands}
updateCheckPromise={updateCheckPromise}
/>,
renderContextWithExitOnCtrlC,
)
Expand Down Expand Up @@ -685,6 +658,7 @@ async function parseArgs(
initialUpdateVersion={updateInfo.version}
initialUpdateCommands={updateInfo.commands}
initialMessages={initialMessages}
updateCheckPromise={updateCheckPromise}
/>,
renderContext,
)
Expand Down
6 changes: 6 additions & 0 deletions src/tools/system/BashTool/BashTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Tool, ValidationResult, ToolUseContext } from '@tool'
import { splitCommand } from '@utils/commands'
import { isInDirectory } from '@utils/fs/file'
import { logError } from '@utils/log'
import { logStartupProfileDuration } from '@utils/config/startupProfile'
import { createAssistantMessage } from '@utils/messages'
import { BunShell } from '@utils/bun/shell'
import { getBunShellSandboxPlan } from '@utils/sandbox/bunShellSandboxPlan'
Expand Down Expand Up @@ -700,6 +701,7 @@ export const BashTool = {
}

if (race.kind === 'done') {
const finalizeStartedAt = Date.now()
const result = race.r

stdout += (result.stdout || '').trim() + EOL
Expand Down Expand Up @@ -743,6 +745,10 @@ export const BashTool = {
interrupted: result.interrupted,
}

logStartupProfileDuration(
'bash_result_finalize',
Date.now() - finalizeStartedAt,
)
yield {
type: 'result',
resultForAssistant: this.renderResultForAssistant(data),
Expand Down
46 changes: 30 additions & 16 deletions src/ui/components/Logo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,32 @@ const DEFAULT_UPDATE_COMMANDS = [
'npm install -g @shareai-lab/kode@latest',
] as const

export function UpdateBanner({
version,
commands,
}: {
version: string
commands?: string[] | null
}): React.ReactNode {
return (
<Box flexDirection="column">
<Text color="yellow">
New version available: {version} (current: {MACRO.VERSION})
</Text>
<Text>Run the following command to update:</Text>
<Text>
{' '}
{commands?.[1] ?? DEFAULT_UPDATE_COMMANDS[1]}
</Text>
{process.platform !== 'win32' && (
<Text dimColor>
Note: you may need to prefix with "sudo" on macOS/Linux.
</Text>
)}
</Box>
)
}

export function Logo({
mcpClients,
isDefaultModel = false,
Expand Down Expand Up @@ -52,22 +78,10 @@ export function Logo({
width={width}
>
{updateBannerVersion ? (
<Box flexDirection="column">
<Text color="yellow">
New version available: {updateBannerVersion} (current:{' '}
{MACRO.VERSION})
</Text>
<Text>Run the following command to update:</Text>
<Text>
{' '}
{updateBannerCommands?.[1] ?? DEFAULT_UPDATE_COMMANDS[1]}
</Text>
{process.platform !== 'win32' && (
<Text dimColor>
Note: you may need to prefix with "sudo" on macOS/Linux.
</Text>
)}
</Box>
<UpdateBanner
version={updateBannerVersion}
commands={updateBannerCommands}
/>
) : null}
<Text>
<Text color={theme.kode}>✻</Text> Welcome to{' '}
Expand Down
Loading
Loading