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 scripts/defines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
*/
export function getMacroDefines(): Record<string, string> {
return {
"MACRO.VERSION": JSON.stringify("2.1.888"),
"MACRO.VERSION": JSON.stringify("4.0.1"),
"MACRO.BUILD_TIME": JSON.stringify(new Date().toISOString()),
"MACRO.FEEDBACK_CHANNEL": JSON.stringify(""),
"MACRO.ISSUES_EXPLAINER": JSON.stringify(""),
Expand Down
140 changes: 135 additions & 5 deletions src/components/ConsoleOAuthFlow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settin
import { Select } from './CustomSelect/select.js'
import { Spinner } from './Spinner.js'
import TextInput from './TextInput.js'
import { fi } from 'zod/v4/locales'
import { ModelPicker } from './ModelPicker.js'
import { useSetAppState } from '../state/AppState.js'

type Props = {
onDone(): void
Expand All @@ -28,6 +29,14 @@ type Props = {
type OAuthStatus =
| { state: 'idle' } // Initial state, waiting to select login method
| { state: 'platform_setup' } // Show platform setup info (Bedrock/Vertex/Foundry)
| {
state: 'costrict_waiting'
url: string
} // CoStrict OAuth: browser opened, waiting for user to login
| {
state: 'costrict_model_select'
models: Array<{ id: string; name?: string }>
} // CoStrict: login done, select a model
| {
state: 'custom_platform'
baseUrl: string
Expand Down Expand Up @@ -73,6 +82,7 @@ export function ConsoleOAuthFlow({
mode = 'login',
forceLoginMethod: forceLoginMethodProp,
}: Props): React.ReactNode {
const setAppState = useSetAppState()
const settings = getSettings_DEPRECATED() || {}
const forceLoginMethod = forceLoginMethodProp ?? settings.forceLoginMethod
const orgUUID = settings.forceLoginOrgUUID
Expand Down Expand Up @@ -273,6 +283,8 @@ export function ConsoleOAuthFlow({
}
// Reset modelType to anthropic when using OAuth login
updateSettingsForSource('userSettings', { modelType: 'anthropic' } as any)
delete process.env.CLAUDE_CODE_USE_COSTRICT
setAppState(prev => ({ ...prev, mainLoopModel: null, mainLoopModelForSession: null }))

setOAuthStatus({ state: 'success' })
void sendNotification(
Expand Down Expand Up @@ -438,6 +450,7 @@ function OAuthStatusMessage({
setLoginWithClaudeAi,
onDone,
}: OAuthStatusMessageProps): React.ReactNode {
const setAppState = useSetAppState()
switch (oauthStatus.state) {
case 'idle':
return (
Expand All @@ -453,6 +466,16 @@ function OAuthStatusMessage({
<Box>
<Select
options={[
{
label: (
<Text>
CoStrict ·{' '}
<Text dimColor>Sign in with CoStrict account</Text>
{'\n'}
</Text>
),
value: 'costrict',
},
{
label: (
<Text>
Expand Down Expand Up @@ -530,7 +553,70 @@ function OAuthStatusMessage({
},
]}
onChange={value => {
if (value === 'custom_platform') {
if (value === 'costrict') {
void (async () => {
try {
const { generateState, getCoStrictBaseURL, buildCoStrictLoginURL, pollLoginToken } = await import('../costrict/provider/auth.js')
const { generateMachineId, saveCoStrictCredentials } = await import('../costrict/provider/credentials.js')
const { extractExpiryFromJWT } = await import('../costrict/provider/token.js')
const { updateSettingsForSource } = await import('../utils/settings/settings.js')

const baseUrl = getCoStrictBaseURL()
const state = generateState()
const machineId = generateMachineId()
const loginUrl = buildCoStrictLoginURL(baseUrl, state, machineId)

// 打开浏览器(复用 csc 的 openBrowser,Windows 用 rundll32 url,OpenURL 避免 & 被 cmd.exe 截断)
const { openBrowser } = await import('../utils/browser.js')
await openBrowser(loginUrl)

setOAuthStatus({ state: 'costrict_waiting', url: loginUrl })

// 轮询等待登录完成
const tokens = await pollLoginToken(baseUrl, state, machineId)

// 保存凭证
const expiryDate = extractExpiryFromJWT(tokens.access_token)
await saveCoStrictCredentials({
id: 'csc',
name: 'CSC Auth',
access_token: tokens.access_token,
refresh_token: tokens.refresh_token,
state,
machine_id: machineId,
base_url: baseUrl,
expiry_date: expiryDate,
updated_at: new Date().toISOString(),
expired_at: expiryDate ? new Date(expiryDate).toISOString() : undefined,
})

// 设置 modelType 为 costrict
updateSettingsForSource('userSettings', { modelType: 'costrict' as any } as any)
process.env.CLAUDE_CODE_USE_COSTRICT = '1'

// 预取模型列表,填充同步缓存,并进入模型选择界面
try {
const { fetchCoStrictModels } = await import('../costrict/provider/models.js')
const models = await fetchCoStrictModels(baseUrl, tokens.access_token)
if (models.length > 0) {
setOAuthStatus({ state: 'costrict_model_select', models })
return
}
} catch {
// 预取失败,直接进入 success
}

setOAuthStatus({ state: 'success' })
void onDone()
} catch (err: any) {
setOAuthStatus({
state: 'error',
message: err.message || String(err),
toRetry: { state: 'idle' },
})
}
})()
} else if (value === 'custom_platform') {
logEvent('tengu_custom_platform_selected', {})
setOAuthStatus({
state: 'custom_platform',
Expand Down Expand Up @@ -696,10 +782,12 @@ function OAuthStatusMessage({
})
} else {
for (const [k, v] of Object.entries(env)) process.env[k] = v
delete process.env.CLAUDE_CODE_USE_COSTRICT
setAppState(prev => ({ ...prev, mainLoopModel: null, mainLoopModelForSession: null }))
setOAuthStatus({ state: 'success' })
void onDone()
}
}, [activeField, inputValue, displayValues, setOAuthStatus, onDone])
}, [activeField, inputValue, displayValues, setOAuthStatus, onDone, setAppState])

const handleEnter = useCallback(() => {
const idx = FIELDS.indexOf(activeField)
Expand Down Expand Up @@ -916,10 +1004,12 @@ function OAuthStatusMessage({
})
} else {
for (const [k, v] of Object.entries(env)) process.env[k] = v
delete process.env.CLAUDE_CODE_USE_COSTRICT
setAppState(prev => ({ ...prev, mainLoopModel: null, mainLoopModelForSession: null }))
setOAuthStatus({ state: 'success' })
void onDone()
}
}, [activeField, openaiInputValue, openaiDisplayValues, setOAuthStatus, onDone])
}, [activeField, openaiInputValue, openaiDisplayValues, setOAuthStatus, onDone, setAppState])

const handleOpenAIEnter = useCallback(() => {
const idx = OPENAI_FIELDS.indexOf(activeField)
Expand Down Expand Up @@ -1149,10 +1239,12 @@ function OAuthStatusMessage({
})
} else {
for (const [k, v] of Object.entries(env)) process.env[k] = v
delete process.env.CLAUDE_CODE_USE_COSTRICT
setAppState(prev => ({ ...prev, mainLoopModel: null, mainLoopModelForSession: null }))
setOAuthStatus({ state: 'success' })
void onDone()
}
}, [activeField, geminiInputValue, geminiDisplayValues, onDone, setOAuthStatus])
}, [activeField, geminiInputValue, geminiDisplayValues, onDone, setOAuthStatus, setAppState])

const handleGeminiEnter = useCallback(() => {
const idx = GEMINI_FIELDS.indexOf(activeField)
Expand Down Expand Up @@ -1275,6 +1367,44 @@ function OAuthStatusMessage({
)
}

case 'costrict_waiting':
return (
<Box flexDirection="column" gap={1}>
<Text>
Opening browser for CoStrict login. If it does not open
automatically, copy and paste this URL:
</Text>
<Box marginY={1}>
<Text color="cyan">{oauthStatus.url}</Text>
</Box>
<Text dimColor>Waiting for authentication...</Text>
</Box>
)

case 'costrict_model_select': {
const sortedModels = [...oauthStatus.models].sort((a, b) => a.id.localeCompare(b.id))
return (
<ModelPicker
initial={sortedModels[0]?.id ?? null}
headerText="Login successful. Select a CoStrict model to use:"
onSelect={(model) => {
const selected = model ?? sortedModels[0]?.id ?? ''
process.env.COSTRICT_MODEL = selected
setAppState(prev => ({ ...prev, mainLoopModel: selected, mainLoopModelForSession: null }))
setOAuthStatus({ state: 'success' })
void onDone()
}}
onCancel={() => {
const selected = sortedModels[0]?.id ?? ''
process.env.COSTRICT_MODEL = selected
setAppState(prev => ({ ...prev, mainLoopModel: selected, mainLoopModelForSession: null }))
setOAuthStatus({ state: 'success' })
void onDone()
}}
/>
)
}

case 'platform_setup':
return (
<Box flexDirection="column" gap={1} marginTop={1}>
Expand Down
99 changes: 11 additions & 88 deletions src/components/LogoV2/Clawd.tsx
Original file line number Diff line number Diff line change
@@ -1,98 +1,21 @@
import * as React from 'react'
import { Box, Text } from '@anthropic/ink'
import { env } from '../../utils/env.js'
import * as React from 'react';
import { Box, Text } from '@anthropic/ink';

export type ClawdPose =
| 'default'
| 'arms-up' // both arms raised (used during jump)
| 'look-left' // both pupils shifted left
| 'look-right' // both pupils shifted right
export type ClawdPose = 'default' | 'arms-up' | 'look-left' | 'look-right';

type Props = {
pose?: ClawdPose
}

// Standard-terminal pose fragments. Each row is split into segments so we can
// vary only the parts that change (eyes, arms) while keeping the body/bg spans
// stable. All poses end up 9 cols wide.
//
// arms-up: the row-2 arm shapes (▝▜ / ▛▘) move to row 1 as their
// bottom-heavy mirrors (▗▟ / ▙▖) — same silhouette, one row higher.
//
// look-* use top-quadrant eye chars (▙/▟) so both eyes change from the
// default (▛/▜, bottom pupils) — otherwise only one eye would appear to move.
type Segments = {
/** row 1 left (no bg): optional raised arm + side */
r1L: string
/** row 1 eyes (with bg): left-eye, forehead, right-eye */
r1E: string
/** row 1 right (no bg): side + optional raised arm */
r1R: string
/** row 2 left (no bg): arm + body curve */
r2L: string
/** row 2 right (no bg): body curve + arm */
r2R: string
}

const POSES: Record<ClawdPose, Segments> = {
default: { r1L: ' ▐', r1E: '▛███▜', r1R: '▌', r2L: '▝▜', r2R: '▛▘' },
'look-left': { r1L: ' ▐', r1E: '▟███▟', r1R: '▌', r2L: '▝▜', r2R: '▛▘' },
'look-right': { r1L: ' ▐', r1E: '▙███▙', r1R: '▌', r2L: '▝▜', r2R: '▛▘' },
'arms-up': { r1L: '▗▟', r1E: '▛███▜', r1R: '▙▖', r2L: ' ▜', r2R: '▛ ' },
}
pose?: ClawdPose;
};

// Apple Terminal uses a bg-fill trick (see below), so only eye poses make
// sense. Arm poses fall back to default.
const APPLE_EYES: Record<ClawdPose, string> = {
default: ' ▗ ▖ ',
'look-left': ' ▘ ▘ ',
'look-right': ' ▝ ▝ ',
'arms-up': ' ▗ ▖ ',
}
// CoStrict ASCII art 大字标志,两行块字符风格:
// █▀▀ █▀█ █▀ ▀█▀ █▀█ █ █▀▀ ▀█▀
// █▄▄ █▄█ ▄█ █ █▀▄ █ █▄▄ █

export function Clawd({ pose = 'default' }: Props = {}): React.ReactNode {
if (env.terminal === 'Apple_Terminal') {
return <AppleTerminalClawd pose={pose} />
}
const p = POSES[pose]
return (
<Box flexDirection="column">
<Text>
<Text color="clawd_body">{p.r1L}</Text>
<Text color="clawd_body" backgroundColor="clawd_background">
{p.r1E}
</Text>
<Text color="clawd_body">{p.r1R}</Text>
</Text>
<Text>
<Text color="clawd_body">{p.r2L}</Text>
<Text color="clawd_body" backgroundColor="clawd_background">
█████
</Text>
<Text color="clawd_body">{p.r2R}</Text>
</Text>
<Text color="clawd_body">
{' '}▘▘ ▝▝{' '}
</Text>
</Box>
)
}

function AppleTerminalClawd({ pose }: { pose: ClawdPose }): React.ReactNode {
// Apple's Terminal renders vertical space between chars by default.
// It does NOT render vertical space between background colors
// so we use background color to draw the main shape.
return (
<Box flexDirection="column" alignItems="center">
<Text>
<Text color="clawd_body">▗</Text>
<Text color="clawd_background" backgroundColor="clawd_body">
{APPLE_EYES[pose]}
</Text>
<Text color="clawd_body">▖</Text>
</Text>
<Text backgroundColor="clawd_body">{' '.repeat(7)}</Text>
<Text color="clawd_body">▘▘ ▝▝</Text>
<Text color="claudeBlue_FOR_SYSTEM_SPINNER">{'█▀▀ █▀█ █▀ ▀█▀ █▀█ █ █▀▀ ▀█▀'}</Text>
<Text color="claudeBlue_FOR_SYSTEM_SPINNER">{'█▄▄ █▄█ ▄█ █ █▀▄ █ █▄▄ █ '}</Text>
</Box>
)
);
}
Loading