mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-15 12:55:51 +00:00
Merge branch 'claude-code-best:main' into main
This commit is contained in:
@@ -706,11 +706,12 @@ function OAuthStatusMessage({
|
||||
|
||||
const handleEnter = useCallback(() => {
|
||||
const idx = FIELDS.indexOf(activeField)
|
||||
setOAuthStatus(buildState(activeField, inputValue))
|
||||
if (idx === FIELDS.length - 1) {
|
||||
setOAuthStatus(buildState(activeField, inputValue))
|
||||
doSave()
|
||||
} else {
|
||||
const next = FIELDS[idx + 1]!
|
||||
setOAuthStatus(buildState(activeField, inputValue, next))
|
||||
setInputValue(displayValues[next] ?? '')
|
||||
setInputCursorOffset((displayValues[next] ?? '').length)
|
||||
}
|
||||
@@ -726,7 +727,7 @@ function OAuthStatusMessage({
|
||||
setInputCursorOffset((displayValues[FIELDS[idx + 1]!] ?? '').length)
|
||||
}
|
||||
},
|
||||
{ context: 'Tabs' },
|
||||
{ context: 'FormField' },
|
||||
)
|
||||
useKeybinding(
|
||||
'tabs:previous',
|
||||
@@ -738,7 +739,7 @@ function OAuthStatusMessage({
|
||||
setInputCursorOffset((displayValues[FIELDS[idx - 1]!] ?? '').length)
|
||||
}
|
||||
},
|
||||
{ context: 'Tabs' },
|
||||
{ context: 'FormField' },
|
||||
)
|
||||
useKeybinding(
|
||||
'confirm:no',
|
||||
@@ -799,7 +800,7 @@ function OAuthStatusMessage({
|
||||
{renderRow('opus_model', 'Opus ')}
|
||||
</Box>
|
||||
<Text dimColor>
|
||||
Tab to switch · Enter on last field to save · Esc to go back
|
||||
↑↓/Tab to switch · Enter on last field to save · Esc to go back
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
@@ -925,11 +926,12 @@ function OAuthStatusMessage({
|
||||
|
||||
const handleOpenAIEnter = useCallback(() => {
|
||||
const idx = OPENAI_FIELDS.indexOf(activeField)
|
||||
setOAuthStatus(buildOpenAIState(activeField, openaiInputValue))
|
||||
if (idx === OPENAI_FIELDS.length - 1) {
|
||||
setOAuthStatus(buildOpenAIState(activeField, openaiInputValue))
|
||||
doOpenAISave()
|
||||
} else {
|
||||
const next = OPENAI_FIELDS[idx + 1]!
|
||||
setOAuthStatus(buildOpenAIState(activeField, openaiInputValue, next))
|
||||
setOpenaiInputValue(openaiDisplayValues[next] ?? '')
|
||||
setOpenaiInputCursorOffset((openaiDisplayValues[next] ?? '').length)
|
||||
}
|
||||
@@ -956,7 +958,7 @@ function OAuthStatusMessage({
|
||||
)
|
||||
}
|
||||
},
|
||||
{ context: 'Tabs' },
|
||||
{ context: 'FormField' },
|
||||
)
|
||||
useKeybinding(
|
||||
'tabs:previous',
|
||||
@@ -972,7 +974,7 @@ function OAuthStatusMessage({
|
||||
)
|
||||
}
|
||||
},
|
||||
{ context: 'Tabs' },
|
||||
{ context: 'FormField' },
|
||||
)
|
||||
useKeybinding(
|
||||
'confirm:no',
|
||||
@@ -1037,7 +1039,7 @@ function OAuthStatusMessage({
|
||||
{renderOpenAIRow('opus_model', 'Opus ')}
|
||||
</Box>
|
||||
<Text dimColor>
|
||||
Tab to switch · Enter on last field to save · Esc to go back
|
||||
↑↓/Tab to switch · Enter on last field to save · Esc to go back
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
@@ -1157,11 +1159,12 @@ function OAuthStatusMessage({
|
||||
|
||||
const handleGeminiEnter = useCallback(() => {
|
||||
const idx = GEMINI_FIELDS.indexOf(activeField)
|
||||
setOAuthStatus(buildGeminiState(activeField, geminiInputValue))
|
||||
if (idx === GEMINI_FIELDS.length - 1) {
|
||||
setOAuthStatus(buildGeminiState(activeField, geminiInputValue))
|
||||
doGeminiSave()
|
||||
} else {
|
||||
const next = GEMINI_FIELDS[idx + 1]!
|
||||
setOAuthStatus(buildGeminiState(activeField, geminiInputValue, next))
|
||||
setGeminiInputValue(geminiDisplayValues[next] ?? '')
|
||||
setGeminiInputCursorOffset((geminiDisplayValues[next] ?? '').length)
|
||||
}
|
||||
@@ -1188,7 +1191,7 @@ function OAuthStatusMessage({
|
||||
)
|
||||
}
|
||||
},
|
||||
{ context: 'Tabs' },
|
||||
{ context: 'FormField' },
|
||||
)
|
||||
useKeybinding(
|
||||
'tabs:previous',
|
||||
@@ -1204,7 +1207,7 @@ function OAuthStatusMessage({
|
||||
)
|
||||
}
|
||||
},
|
||||
{ context: 'Tabs' },
|
||||
{ context: 'FormField' },
|
||||
)
|
||||
useKeybinding(
|
||||
'confirm:no',
|
||||
@@ -1269,7 +1272,7 @@ function OAuthStatusMessage({
|
||||
{renderGeminiRow('opus_model', 'Opus ')}
|
||||
</Box>
|
||||
<Text dimColor>
|
||||
Tab to switch · Enter on last field to save · Esc to go back
|
||||
↑↓/Tab to switch · Enter on last field to save · Esc to go back
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
#!/usr/bin/env bun
|
||||
import { feature } from 'bun:bundle'
|
||||
|
||||
// Runtime fallback for MACRO.* when not injected by build/dev defines.
|
||||
// This happens when running cli.tsx directly (not via `bun run dev` or built dist/).
|
||||
if (typeof globalThis.MACRO === 'undefined') {
|
||||
;(globalThis as any).MACRO = {
|
||||
VERSION: process.env.CLAUDE_CODE_VERSION || '2.1.888',
|
||||
BUILD_TIME: new Date().toISOString(),
|
||||
FEEDBACK_CHANNEL: '',
|
||||
ISSUES_EXPLAINER: '',
|
||||
NATIVE_PACKAGE_URL: '',
|
||||
PACKAGE_URL: '',
|
||||
VERSION_CHANGELOG: '',
|
||||
}
|
||||
}
|
||||
|
||||
// Bugfix for corepack auto-pinning, which adds yarnpkg to peoples' package.jsons
|
||||
// eslint-disable-next-line custom-rules/no-top-level-side-effects
|
||||
process.env.COREPACK_ENABLE_AUTO_PIN = '0'
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isEnvTruthy } from 'src/utils/envUtils.js'
|
||||
import { useStartupNotification } from './useStartupNotification.js'
|
||||
|
||||
const NPM_DEPRECATION_MESSAGE =
|
||||
'Claude Code has switched from npm to native installer. Run `claude install` or see https://docs.anthropic.com/en/docs/claude-code/getting-started for more options.'
|
||||
''
|
||||
|
||||
export function useNpmDeprecationNotification(): void {
|
||||
useStartupNotification(async () => {
|
||||
|
||||
@@ -147,6 +147,16 @@ export const DEFAULT_BINDINGS: KeybindingBlock[] = [
|
||||
'ctrl+d': 'permission:toggleDebug',
|
||||
},
|
||||
},
|
||||
{
|
||||
context: 'FormField',
|
||||
bindings: {
|
||||
// Form field vertical navigation (login/setup panels)
|
||||
tab: 'tabs:next',
|
||||
'shift+tab': 'tabs:previous',
|
||||
up: 'tabs:previous',
|
||||
down: 'tabs:next',
|
||||
},
|
||||
},
|
||||
{
|
||||
context: 'Tabs',
|
||||
bindings: {
|
||||
|
||||
@@ -199,4 +199,69 @@ describe('anthropicMessagesToGemini', () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('converts base64 image to inlineData', () => {
|
||||
const result = anthropicMessagesToGemini(
|
||||
[makeUserMsg([
|
||||
{ type: 'text', text: 'describe this' },
|
||||
{
|
||||
type: 'image',
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'image/png',
|
||||
data: 'iVBORw0KGgo=',
|
||||
},
|
||||
},
|
||||
])],
|
||||
[] as any,
|
||||
)
|
||||
expect(result.contents).toEqual([
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{ text: 'describe this' },
|
||||
{ inlineData: { mimeType: 'image/png', data: 'iVBORw0KGgo=' } },
|
||||
],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('converts url image to text fallback', () => {
|
||||
const result = anthropicMessagesToGemini(
|
||||
[makeUserMsg([
|
||||
{
|
||||
type: 'image',
|
||||
source: {
|
||||
type: 'url',
|
||||
url: 'https://example.com/img.png',
|
||||
},
|
||||
},
|
||||
])],
|
||||
[] as any,
|
||||
)
|
||||
expect(result.contents).toEqual([
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: '[image: https://example.com/img.png]' }],
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test('defaults to image/png when media_type is missing', () => {
|
||||
const result = anthropicMessagesToGemini(
|
||||
[makeUserMsg([
|
||||
{
|
||||
type: 'image',
|
||||
source: {
|
||||
type: 'base64',
|
||||
data: 'ABC123',
|
||||
},
|
||||
},
|
||||
])],
|
||||
[] as any,
|
||||
)
|
||||
expect(result.contents[0].parts[0]).toEqual({
|
||||
inlineData: { mimeType: 'image/png', data: 'ABC123' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -113,6 +113,26 @@ function convertUserContentBlockToGeminiParts(
|
||||
]
|
||||
}
|
||||
|
||||
// 将 Anthropic image 块转换为 Gemini inlineData
|
||||
if (block.type === 'image') {
|
||||
const source = block.source as Record<string, unknown> | undefined
|
||||
if (source?.type === 'base64' && typeof source.data === 'string') {
|
||||
const mediaType = (source.media_type as string) || 'image/png'
|
||||
return [
|
||||
{
|
||||
inlineData: {
|
||||
mimeType: mediaType,
|
||||
data: source.data,
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
// url 类型的图片,Gemini 不直接支持,转为文本描述
|
||||
if (source?.type === 'url' && typeof source.url === 'string') {
|
||||
return createTextGeminiParts(`[image: ${source.url}]`)
|
||||
}
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,18 @@ export type GeminiFunctionResponse = {
|
||||
response?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export type GeminiInlineData = {
|
||||
mimeType: string
|
||||
data: string
|
||||
}
|
||||
|
||||
export type GeminiPart = {
|
||||
text?: string
|
||||
thought?: boolean
|
||||
thoughtSignature?: string
|
||||
functionCall?: GeminiFunctionCall
|
||||
functionResponse?: GeminiFunctionResponse
|
||||
inlineData?: GeminiInlineData
|
||||
}
|
||||
|
||||
export type GeminiContent = {
|
||||
|
||||
@@ -154,4 +154,98 @@ describe('anthropicMessagesToOpenAI', () => {
|
||||
expect((result[2] as any).tool_calls).toBeDefined()
|
||||
expect(result[3].role).toBe('tool')
|
||||
})
|
||||
|
||||
test('converts base64 image to image_url', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{ type: 'text', text: 'what is this?' },
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'image/png',
|
||||
data: 'iVBORw0KGgo=',
|
||||
},
|
||||
},
|
||||
])],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{
|
||||
role: 'user',
|
||||
content: [
|
||||
{ type: 'text', text: 'what is this?' },
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: 'data:image/png;base64,iVBORw0KGgo=' },
|
||||
},
|
||||
],
|
||||
}])
|
||||
})
|
||||
|
||||
test('converts url image to image_url', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'url',
|
||||
url: 'https://example.com/img.png',
|
||||
},
|
||||
},
|
||||
])],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: 'https://example.com/img.png' },
|
||||
},
|
||||
],
|
||||
}])
|
||||
})
|
||||
|
||||
test('converts image-only message without text', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'base64',
|
||||
media_type: 'image/jpeg',
|
||||
data: '/9j/4AAQ',
|
||||
},
|
||||
},
|
||||
])],
|
||||
[] as any,
|
||||
)
|
||||
expect(result).toEqual([{
|
||||
role: 'user',
|
||||
content: [
|
||||
{
|
||||
type: 'image_url',
|
||||
image_url: { url: 'data:image/jpeg;base64,/9j/4AAQ' },
|
||||
},
|
||||
],
|
||||
}])
|
||||
})
|
||||
|
||||
test('defaults to image/png when media_type is missing', () => {
|
||||
const result = anthropicMessagesToOpenAI(
|
||||
[makeUserMsg([
|
||||
{
|
||||
type: 'image' as const,
|
||||
source: {
|
||||
type: 'base64',
|
||||
data: 'ABC123',
|
||||
},
|
||||
},
|
||||
])],
|
||||
[] as any,
|
||||
)
|
||||
expect((result[0].content as any[])[0].image_url.url).toBe(
|
||||
'data:image/png;base64,ABC123',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -75,6 +75,7 @@ function convertInternalUserMessage(
|
||||
} else if (Array.isArray(content)) {
|
||||
const textParts: string[] = []
|
||||
const toolResults: BetaToolResultBlockParam[] = []
|
||||
const imageParts: Array<{ type: 'image_url'; image_url: { url: string } }> = []
|
||||
|
||||
for (const block of content) {
|
||||
if (typeof block === 'string') {
|
||||
@@ -83,11 +84,26 @@ function convertInternalUserMessage(
|
||||
textParts.push(block.text)
|
||||
} else if (block.type === 'tool_result') {
|
||||
toolResults.push(block as BetaToolResultBlockParam)
|
||||
} else if (block.type === 'image') {
|
||||
const imagePart = convertImageBlockToOpenAI(block as Record<string, unknown>)
|
||||
if (imagePart) {
|
||||
imageParts.push(imagePart)
|
||||
}
|
||||
}
|
||||
// Skip image, document, thinking, cache_edits, etc.
|
||||
}
|
||||
|
||||
if (textParts.length > 0) {
|
||||
// 如果有图片,构建多模态 content 数组
|
||||
if (imageParts.length > 0) {
|
||||
const multiContent: Array<{ type: 'text'; text: string } | { type: 'image_url'; image_url: { url: string } }> = []
|
||||
if (textParts.length > 0) {
|
||||
multiContent.push({ type: 'text', text: textParts.join('\n') })
|
||||
}
|
||||
multiContent.push(...imageParts)
|
||||
result.push({
|
||||
role: 'user',
|
||||
content: multiContent,
|
||||
} satisfies ChatCompletionUserMessageParam)
|
||||
} else if (textParts.length > 0) {
|
||||
result.push({
|
||||
role: 'user',
|
||||
content: textParts.join('\n'),
|
||||
@@ -182,3 +198,38 @@ function convertInternalAssistantMessage(
|
||||
|
||||
return [result]
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Anthropic image 块转换为 OpenAI image_url 格式。
|
||||
*
|
||||
* Anthropic 格式: { type: "image", source: { type: "base64", media_type: "image/png", data: "..." } }
|
||||
* OpenAI 格式: { type: "image_url", image_url: { url: "data:image/png;base64,..." } }
|
||||
*/
|
||||
function convertImageBlockToOpenAI(
|
||||
block: Record<string, unknown>,
|
||||
): { type: 'image_url'; image_url: { url: string } } | null {
|
||||
const source = block.source as Record<string, unknown> | undefined
|
||||
if (!source) return null
|
||||
|
||||
if (source.type === 'base64' && typeof source.data === 'string') {
|
||||
const mediaType = (source.media_type as string) || 'image/png'
|
||||
return {
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: `data:${mediaType};base64,${source.data}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// url 类型的图片直接传递
|
||||
if (source.type === 'url' && typeof source.url === 'string') {
|
||||
return {
|
||||
type: 'image_url',
|
||||
image_url: {
|
||||
url: source.url,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export function isFullscreenEnvEnabled(): boolean {
|
||||
}
|
||||
return false
|
||||
}
|
||||
return process.env.USER_TYPE === 'ant'
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user