diff --git a/docs/ui-rendering.md b/docs/ui-rendering.md
new file mode 100644
index 000000000..51e5779f9
--- /dev/null
+++ b/docs/ui-rendering.md
@@ -0,0 +1,121 @@
+# UI 渲染与界面层
+
+> 记录 Claude Code 终端 UI(Ink 框架)的渲染行为、布局模式、常见问题及修复。
+
+## LogoV2 启动欢迎界面
+
+### 组件结构
+
+```
+LogoV2.tsx (src/components/LogoV2/)
+├── CondensedLogo.tsx — 缩略模式(无边框,日常最常见)
+├── LogoV2.tsx — 完整模式(有边框)/ 紧凑模式(有边框)
+├── CondensedLogo.tsx — 缩略模式(无边框,日常最常见)
+├── Clawd.tsx — ASCII 猫吉祥物
+├── AnimatedClawd.tsx — 动画版 Clawd
+├── FeedColumn.tsx — 右侧信息流(活动记录/更新日志)
+└── WelcomeV2.tsx — 首次 Onboarding 欢迎文本
+```
+
+### 三种渲染模式
+
+| 模式 | 触发条件 | 边框 | 说明 |
+|------|---------|------|------|
+| **缩略模式** (Condensed) | ~~无 Release Notes 且无首次引导~~ | ❌ | **已永久关闭**(2026-04-10),不再进入 |
+| **紧凑模式** (Compact) | 终端宽度 < 70 列 | ✅ | 紫色圆角边框,内容居中 |
+| **完整模式** (Full) | 默认路径 | ✅ | 紫色圆角边框,双栏布局 + 信息流 |
+
+> **注**:缩略模式已在 `LogoV2.tsx` 中永久关闭(`isCondensedMode = false`),所有启动都走完整模式或紧凑模式。
+
+### 模式判断逻辑(源码:`src/components/LogoV2/LogoV2.tsx`)
+
+```typescript
+// 原始逻辑:三种模式切换
+const isCondensedMode =
+ !hasReleaseNotes &&
+ !showOnboarding &&
+ !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO)
+
+// 环境变量强制:始终显示完整模式
+// CLAUDE_CODE_FORCE_FULL_LOGO=1 bun run dev
+```
+
+**CondensedLogo 内部**(`src/components/LogoV2/CondensedLogo.tsx`):无任何边框,纯文本 + ASCII 猫。
+
+### 边框渲染机制
+
+边框由 `@anthropic/ink` 的 `Box` 组件通过 `cli-boxes` 库绘制:
+
+- `borderStyle="round"` → 使用 `cli-boxes` 的 `round` 样式(`╭─╮` `│` `╰─╯`)
+- `borderColor="claude"` → 紫色主题色
+- `borderText` → 边框顶部嵌入标题文本 "Claude Code"
+
+详见 `packages/@ant/ink/src/core/render-border.ts`。
+
+## 常见问题
+
+### 问题:启动时欢迎边框时有时无
+
+**原因**:日常启动(无 Release Notes、非首次使用)走缩略模式,不显示边框。
+
+**修复**:在 `LogoV2.tsx` 中永久关闭缩略模式:
+
+```diff
+- const isCondensedMode =
+- !hasReleaseNotes &&
+- !showOnboarding &&
+- !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO)
++ const isCondensedMode = false
+
+- if (
+- !hasReleaseNotes &&
+- !showOnboarding &&
+- !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO)
+- ) {
++ if (false) {
+ return
+ }
+```
+
+修改后,无论何种情况启动,都会显示完整模式的紫色圆角边框。
+
+**影响**:
+- 每次启动都会渲染完整的边框 + 信息流(活动记录或更新日志)
+- 轻微增加启动渲染开销(从 0 边框到有边框双栏)
+- 不再走 CondensedLogo(节省了 GuestPassesUpsell 等副作用计数逻辑)
+
+### 问题:终端宽度不足导致边框变形
+
+**原因**:`getLayoutMode(columns)` 在终端 < 70 列时切换到 compact 模式。
+
+**参考**:`src/utils/logoV2Utils.ts` — `getLayoutMode(columns: number): LayoutMode`
+
+### 问题:Recent Activity 显示 "No recent activity"
+
+**原因**:`src/setup.ts` 中 `getRecentActivity()` 只在 `hasReleaseNotes` 为 true 时才调用。日常启动(无新 Release Notes)时,`cachedActivity` 始终为空数组。
+
+**修复**(`src/setup.ts` 第 383-395 行):
+
+```diff
+ if (!isBareMode()) {
+- const { hasReleaseNotes } = await checkForReleaseNotes(
+- getGlobalConfig().lastReleaseNotesSeen,
+- )
+- if (hasReleaseNotes) {
+- await getRecentActivity()
+- }
++ // Populate release notes cache (side effect: fetches changelog if needed)
++ void checkForReleaseNotes(getGlobalConfig().lastReleaseNotesSeen)
++ // Load recent activity unconditionally (not tied to release notes)
++ try {
++ await getRecentActivity()
++ } catch (error) {
++ logError('Failed to load recent activity:', error)
++ }
+ }
+```
+
+**关键变化**:
+1. `getRecentActivity()` 从 `if (hasReleaseNotes)` 块中移出,无条件调用
+2. 增加 try-catch 防止损坏的会话文件阻塞启动
+3. `checkForReleaseNotes` 改为 `void` 调用(保留 cache 填充的副作用,但不阻塞等待结果)
diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx
index bd1dd5d1e..091428c96 100644
--- a/src/components/ConsoleOAuthFlow.tsx
+++ b/src/components/ConsoleOAuthFlow.tsx
@@ -55,6 +55,15 @@ type OAuthStatus =
opusModel: string
activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
} // Gemini Generate Content API platform
+ | {
+ state: 'alibaba_coding_plan'
+ baseUrl: string
+ apiKey: string
+ opusModel: string
+ sonnetModel: string
+ haikuModel: string
+ activeField: 'api_key' | 'base_url' | 'opus_model' | 'sonnet_model' | 'haiku_model'
+ } // Alibaba Cloud Bailian Coding Plan (OpenAI compatible)
| { state: 'ready_to_start' } // Flow started, waiting for browser to open
| { state: 'waiting_for_login'; url: string } // Browser opened, waiting for user to login
| { state: 'creating_api_key' } // Got access token, creating API key
@@ -485,6 +494,16 @@ function OAuthStatusMessage({
),
value: 'gemini_api',
},
+ {
+ label: (
+
+ 阿里云百炼 Coding Plan ·{' '}
+ Anthropic 兼容 API
+ {'\n'}
+
+ ),
+ value: 'alibaba_coding_plan',
+ },
{
label: (
@@ -563,6 +582,17 @@ function OAuthStatusMessage({
opusModel: process.env.GEMINI_DEFAULT_OPUS_MODEL ?? '',
activeField: 'base_url',
})
+ } else if (value === 'alibaba_coding_plan') {
+ logEvent('tengu_alibaba_coding_plan_selected', {})
+ setOAuthStatus({
+ state: 'alibaba_coding_plan',
+ baseUrl: process.env.ANTHROPIC_BASE_URL ?? 'https://coding.dashscope.aliyuncs.com/apps/anthropic',
+ apiKey: process.env.ANTHROPIC_AUTH_TOKEN ?? '',
+ opusModel: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? 'qwen3-max-2026-01-23',
+ sonnetModel: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? 'qwen3.6-plus',
+ haikuModel: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? 'glm-5',
+ activeField: 'api_key',
+ })
} else if (value === 'platform') {
logEvent('tengu_oauth_platform_selected', {})
setOAuthStatus({ state: 'platform_setup' })
@@ -1275,6 +1305,262 @@ function OAuthStatusMessage({
)
}
+ case 'alibaba_coding_plan':
+ {
+ type AlibabaField = 'api_key' | 'base_url' | 'opus_model' | 'sonnet_model' | 'haiku_model'
+ const ALIBABA_FIELDS: AlibabaField[] = [
+ 'api_key',
+ 'base_url',
+ 'opus_model',
+ 'sonnet_model',
+ 'haiku_model',
+ ]
+
+ // 阿里云百炼模型映射(Claude 家族 → 百炼模型)
+ const BAILIAN_MODEL_MAP = {
+ opus: ['qwen3-max-2026-01-23', 'qwen3.6-plus'],
+ sonnet: ['qwen3.6-plus', 'qwen3-coder-next', 'kimi-k2.5'],
+ haiku: ['glm-5', 'MiniMax-M2.5', 'qwen3-coder-plus'],
+ }
+
+ const al = oauthStatus as {
+ state: 'alibaba_coding_plan'
+ activeField: AlibabaField
+ baseUrl: string
+ apiKey: string
+ opusModel: string
+ sonnetModel: string
+ haikuModel: string
+ }
+ const { activeField, baseUrl, apiKey, opusModel, sonnetModel, haikuModel } = al
+ const alibabaDisplayValues: Record = {
+ api_key: apiKey,
+ base_url: baseUrl,
+ opus_model: opusModel,
+ sonnet_model: sonnetModel,
+ haiku_model: haikuModel,
+ }
+
+ const [alibabaInputValue, setAlibabaInputValue] = useState(() => alibabaDisplayValues[activeField])
+ const [alibabaInputCursorOffset, setAlibabaInputCursorOffset] = useState(
+ () => alibabaDisplayValues[activeField].length,
+ )
+
+ const buildAlibabaState = useCallback(
+ (field: AlibabaField, value: string, newActive?: AlibabaField) => {
+ const s = {
+ state: 'alibaba_coding_plan' as const,
+ activeField: newActive ?? activeField,
+ baseUrl,
+ apiKey,
+ opusModel,
+ sonnetModel,
+ haikuModel,
+ }
+ switch (field) {
+ case 'api_key':
+ return { ...s, apiKey: value }
+ case 'base_url':
+ return { ...s, baseUrl: value }
+ case 'opus_model':
+ return { ...s, opusModel: value }
+ case 'sonnet_model':
+ return { ...s, sonnetModel: value }
+ case 'haiku_model':
+ return { ...s, haikuModel: value }
+ }
+ },
+ [activeField, baseUrl, apiKey, opusModel, sonnetModel, haikuModel],
+ )
+
+ const switchAlibabaField = useCallback(
+ (target: AlibabaField) => {
+ setOAuthStatus(buildAlibabaState(activeField, alibabaInputValue, target))
+ setAlibabaInputValue(alibabaDisplayValues[target] ?? '')
+ setAlibabaInputCursorOffset((alibabaDisplayValues[target] ?? '').length)
+ },
+ [activeField, alibabaInputValue, alibabaDisplayValues, buildAlibabaState, setOAuthStatus],
+ )
+
+ const doAlibabaSave = useCallback(() => {
+ const finalVals = { ...alibabaDisplayValues, [activeField]: alibabaInputValue }
+ const env: Record = {}
+
+ if (finalVals.base_url) {
+ try {
+ new URL(finalVals.base_url)
+ } catch {
+ setOAuthStatus({
+ state: 'error',
+ message: '无效的 Base URL:请输入完整的 URL(如 https://coding.dashscope.aliyuncs.com/apps/anthropic)',
+ toRetry: {
+ state: 'alibaba_coding_plan',
+ baseUrl: '',
+ apiKey: '',
+ opusModel: finalVals.opus_model,
+ sonnetModel: finalVals.sonnet_model,
+ haikuModel: finalVals.haiku_model,
+ activeField: 'base_url',
+ },
+ })
+ return
+ }
+ env.ANTHROPIC_BASE_URL = finalVals.base_url
+ }
+
+ if (finalVals.api_key) env.ANTHROPIC_AUTH_TOKEN = finalVals.api_key
+ if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model
+ if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model
+ if (finalVals.haiku_model) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals.haiku_model
+
+ const { error } = updateSettingsForSource('userSettings', {
+ modelType: 'anthropic' as any,
+ env,
+ } as any)
+ if (error) {
+ setOAuthStatus({
+ state: 'error',
+ message: '保存设置失败,请重试。',
+ toRetry: {
+ state: 'alibaba_coding_plan',
+ baseUrl: finalVals.base_url ?? '',
+ apiKey: finalVals.api_key ?? '',
+ opusModel: finalVals.opus_model ?? 'qwen3-max-2026-01-23',
+ sonnetModel: finalVals.sonnet_model ?? 'qwen3.6-plus',
+ haikuModel: finalVals.haiku_model ?? 'glm-5',
+ activeField: 'api_key',
+ },
+ })
+ } else {
+ for (const [k, v] of Object.entries(env)) process.env[k] = v
+ setOAuthStatus({ state: 'success' })
+ void onDone()
+ }
+ }, [activeField, alibabaInputValue, alibabaDisplayValues, setOAuthStatus, onDone])
+
+ const handleAlibabaEnter = useCallback(() => {
+ const idx = ALIBABA_FIELDS.indexOf(activeField)
+ if (idx === ALIBABA_FIELDS.length - 1) {
+ setOAuthStatus(buildAlibabaState(activeField, alibabaInputValue))
+ doAlibabaSave()
+ } else {
+ const next = ALIBABA_FIELDS[idx + 1]!
+ setOAuthStatus(buildAlibabaState(activeField, alibabaInputValue, next))
+ setAlibabaInputValue(alibabaDisplayValues[next] ?? '')
+ setAlibabaInputCursorOffset((alibabaDisplayValues[next] ?? '').length)
+ }
+ }, [activeField, alibabaInputValue, buildAlibabaState, doAlibabaSave, alibabaDisplayValues, setOAuthStatus])
+
+ useKeybinding(
+ 'tabs:next',
+ () => {
+ const idx = ALIBABA_FIELDS.indexOf(activeField)
+ if (idx < ALIBABA_FIELDS.length - 1) {
+ setOAuthStatus(buildAlibabaState(activeField, alibabaInputValue, ALIBABA_FIELDS[idx + 1]))
+ setAlibabaInputValue(alibabaDisplayValues[ALIBABA_FIELDS[idx + 1]!] ?? '')
+ setAlibabaInputCursorOffset((alibabaDisplayValues[ALIBABA_FIELDS[idx + 1]!] ?? '').length)
+ }
+ },
+ { context: 'FormField' },
+ )
+ useKeybinding(
+ 'tabs:previous',
+ () => {
+ const idx = ALIBABA_FIELDS.indexOf(activeField)
+ if (idx > 0) {
+ setOAuthStatus(buildAlibabaState(activeField, alibabaInputValue, ALIBABA_FIELDS[idx - 1]))
+ setAlibabaInputValue(alibabaDisplayValues[ALIBABA_FIELDS[idx - 1]!] ?? '')
+ setAlibabaInputCursorOffset((alibabaDisplayValues[ALIBABA_FIELDS[idx - 1]!] ?? '').length)
+ }
+ },
+ { context: 'FormField' },
+ )
+ useKeybinding(
+ 'confirm:no',
+ () => {
+ setOAuthStatus({ state: 'idle' })
+ },
+ { context: 'Confirmation' },
+ )
+
+ const alibabaColumns = useTerminalSize().columns - 20
+
+ const renderAlibabaRow = (
+ field: AlibabaField,
+ label: string,
+ opts?: { mask?: boolean },
+ ) => {
+ const active = activeField === field
+ const val = alibabaDisplayValues[field]
+ return (
+
+
+ {` ${label} `}
+
+
+ {active ? (
+
+
+ ▌
+
+ ) : val ? (
+
+ {opts?.mask
+ ? val.slice(0, 8) + '\u00b7'.repeat(Math.max(0, val.length - 8))
+ : val}
+
+ ) : (
+ (按 Enter 输入)
+ )}
+
+ )
+ }
+
+ const borderLine = (active: boolean) => (
+ active
+ ? {'─'.repeat(alibabaColumns + 12)}
+ : {'─'.repeat(alibabaColumns + 12)}
+ )
+
+ return (
+
+ 阿里云百炼 Coding Plan
+
+ 配置阿里云百炼 Anthropic 兼容端点。已自动映射 Claude 三大模型家族到百炼模型。
+
+
+ {renderAlibabaRow('api_key', 'API 密钥', { mask: true })}
+ {borderLine(activeField === 'api_key')}
+ {renderAlibabaRow('base_url', 'Base URL')}
+ {borderLine(activeField === 'base_url')}
+ {renderAlibabaRow('opus_model', 'Opus (最强推理)')}
+ {borderLine(activeField === 'opus_model')}
+ {renderAlibabaRow('sonnet_model', 'Sonnet(均衡通用)')}
+ {borderLine(activeField === 'sonnet_model')}
+ {renderAlibabaRow('haiku_model', 'Haiku (快速轻量)')}
+
+
+ 可用模型:qwen3.6-plus · qwen3.5-plus · qwen3-max · qwen3-coder-next · qwen3-coder-plus · glm-5 · glm-4.7 · kimi-k2.5 · MiniMax-M2.5
+
+
+ ↑↓/Tab 切换字段 · 最后一字段按 Enter 保存 · Esc 返回
+
+
+ )
+ }
+
case 'platform_setup':
return (
diff --git a/src/components/LogoV2/CondensedLogo.tsx b/src/components/LogoV2/CondensedLogo.tsx
index eb048ec2d..b815f0925 100644
--- a/src/components/LogoV2/CondensedLogo.tsx
+++ b/src/components/LogoV2/CondensedLogo.tsx
@@ -2,7 +2,9 @@ import * as React from 'react'
import { type ReactNode, useEffect } from 'react'
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
-import { Box, Text, stringWidth } from '@anthropic/ink'
+import { Box, Text, color, stringWidth } from '@anthropic/ink'
+import { getGlobalConfig } from 'src/utils/config.js'
+import { resolveThemeSetting } from 'src/utils/systemTheme.js'
import { useAppState } from '../../state/AppState.js'
import { getEffortSuffix } from '../../utils/effort.js'
import { truncate } from '../../utils/format.js'
@@ -79,39 +81,56 @@ export function CondensedLogo(): ReactNode {
: textWidth
const truncatedCwd = truncatePath(cwd, Math.max(cwdAvailableWidth, 10))
+ const userTheme = resolveThemeSetting(getGlobalConfig().theme)
+ const borderTitle = color('claude', userTheme)(' Claude Code ')
+
// OffscreenFreeze: the logo sits at the top of the message list and is the
// first thing to enter scrollback. useMainLoopModel() subscribes to model
// changes and getLogoDisplayData() reads getCwd()/subscription state — any
// of which changing while in scrollback would force a full terminal reset.
return (
-
- {isFullscreenEnvEnabled() ? : }
+
+
+ {isFullscreenEnvEnabled() ? : }
- {/* Info */}
-
-
- Claude Code{' '}
- v{truncatedVersion}
-
- {shouldSplit ? (
- <>
- {truncatedModel}
- {truncatedBilling}
- >
- ) : (
+ {/* Info */}
+
+
+ Claude Code{' '}
+ v{truncatedVersion}
+
+ {shouldSplit ? (
+ <>
+ {truncatedModel}
+ {truncatedBilling}
+ >
+ ) : (
+
+ {truncatedModel} · {truncatedBilling}
+
+ )}
- {truncatedModel} · {truncatedBilling}
+ {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd}
- )}
-
- {agentName ? `@${agentName} · ${truncatedCwd}` : truncatedCwd}
-
- {showGuestPassesUpsell && }
- {!showGuestPassesUpsell && showOverageCreditUpsell && (
-
- )}
-
+ {showGuestPassesUpsell && }
+ {!showGuestPassesUpsell && showOverageCreditUpsell && (
+
+ )}
+
+
)
diff --git a/src/components/LogoV2/LogoV2.tsx b/src/components/LogoV2/LogoV2.tsx
index c7dcf4139..e50c41573 100644
--- a/src/components/LogoV2/LogoV2.tsx
+++ b/src/components/LogoV2/LogoV2.tsx
@@ -40,7 +40,6 @@ import { CondensedLogo } from './CondensedLogo.js'
import { OffscreenFreeze } from '../OffscreenFreeze.js'
import { checkForReleaseNotesSync } from '../../utils/releaseNotes.js'
import { getDumpPromptsPath } from 'src/services/api/dumpPrompts.js'
-import { isEnvTruthy } from 'src/utils/envUtils.js'
import {
getStartupPerfLogPath,
isDetailedProfilingEnabled,
@@ -63,6 +62,9 @@ const ChannelsNoticeModule =
? (require('./ChannelsNotice.js') as typeof import('./ChannelsNotice.js'))
: null
/* eslint-enable @typescript-eslint/no-require-imports */
+
+// Extract for safe JSX rendering (avoids TS18047 narrowing issue in JSX)
+const ChannelsNotice = ChannelsNoticeModule?.ChannelsNotice as (typeof ChannelsNoticeModule) extends null ? null : React.ComponentType
import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js'
import {
useShowGuestPassesUpsell,
@@ -130,13 +132,8 @@ export function LogoV2(): React.ReactNode {
}
}, [config, showOnboarding])
- // In condensed mode (early-return below renders ),
- // CondensedLogo's own useEffect handles the impression count. Skipping
- // here avoids double-counting since hooks fire before the early return.
- const isCondensedMode =
- !hasReleaseNotes &&
- !showOnboarding &&
- !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO)
+ // Always show full logo with border (force full logo permanently enabled)
+ const isCondensedMode = false
useEffect(() => {
if (showGuestPassesUpsell && !showOnboarding && !isCondensedMode) {
@@ -177,18 +174,14 @@ export function LogoV2(): React.ReactNode {
LEFT_PANEL_MAX_WIDTH - 20,
)
- // Show condensed logo if no new changelog and not showing onboarding and not forcing full logo
- if (
- !hasReleaseNotes &&
- !showOnboarding &&
- !isEnvTruthy(process.env.CLAUDE_CODE_FORCE_FULL_LOGO)
- ) {
+ // Show full logo with border (force full logo permanently enabled)
+ if (false) {
return (
<>
- {ChannelsNoticeModule && }
+{ChannelsNotice && }
{isDebugMode() && (
Debug mode enabled
@@ -212,9 +205,9 @@ export function LogoV2(): React.ReactNode {
)}
{announcement && (
- {!process.env.IS_DEMO && config.oauthAccount?.organizationName && (
+ {!process.env.IS_DEMO && config.oauthAccount?.organizationName != null && (
- Message from {config.oauthAccount.organizationName}:
+ Message from {config.oauthAccount!.organizationName}:
)}
{announcement}
@@ -307,7 +300,7 @@ export function LogoV2(): React.ReactNode {
- {ChannelsNoticeModule && }
+{ChannelsNotice && }
{showSandboxStatus && (
@@ -323,7 +316,7 @@ export function LogoV2(): React.ReactNode {
const welcomeMessage = formatWelcomeMessage(username)
const modelLine =
- !process.env.IS_DEMO && config.oauthAccount?.organizationName
+ !process.env.IS_DEMO && config.oauthAccount?.organizationName != null
? `${modelDisplayName} · ${billingType} · ${config.oauthAccount.organizationName}`
: `${modelDisplayName} · ${billingType}`
// Calculate cwd width accounting for agent name if present
@@ -435,7 +428,7 @@ export function LogoV2(): React.ReactNode {
- {ChannelsNoticeModule && }
+ {ChannelsNotice && }
{isDebugMode() && (
Debug mode enabled
@@ -459,7 +452,7 @@ export function LogoV2(): React.ReactNode {
)}
{announcement && (
- {!process.env.IS_DEMO && config.oauthAccount?.organizationName && (
+ {!process.env.IS_DEMO && config.oauthAccount?.organizationName != null && (
Message from {config.oauthAccount.organizationName}:
diff --git a/src/setup.ts b/src/setup.ts
index 985e8577a..1543fd3d6 100644
--- a/src/setup.ts
+++ b/src/setup.ts
@@ -384,11 +384,13 @@ export async function setup(
// --bare / SIMPLE: skip — release notes are interactive-UI display data,
// and getRecentActivity() reads up to 10 session JSONL files.
if (!isBareMode()) {
- const { hasReleaseNotes } = await checkForReleaseNotes(
- getGlobalConfig().lastReleaseNotesSeen,
- )
- if (hasReleaseNotes) {
+ // Populate release notes cache (side effect: fetches changelog if needed)
+ void checkForReleaseNotes(getGlobalConfig().lastReleaseNotesSeen)
+ // Load recent activity unconditionally (not tied to release notes)
+ try {
await getRecentActivity()
+ } catch (error) {
+ logError(new Error('Failed to load recent activity', { cause: error }))
}
}
diff --git a/src/utils/context.ts b/src/utils/context.ts
index 5ec51871c..06f5f6202 100644
--- a/src/utils/context.ts
+++ b/src/utils/context.ts
@@ -46,7 +46,14 @@ export function modelSupports1M(model: string): boolean {
return false
}
const canonical = getCanonicalName(model)
- return canonical.includes('claude-sonnet-4') || canonical.includes('opus-4-6')
+ if (canonical.includes('claude-sonnet-4') || canonical.includes('opus-4-6')) {
+ return true
+ }
+ // Third-party 1M models: Alibaba Qwen, etc.
+ if (canonical.startsWith('qwen')) {
+ return true
+ }
+ return false
}
export function getContextWindowForModel(
@@ -83,6 +90,12 @@ export function getContextWindowForModel(
return cap.max_input_tokens
}
+ // Independent 1M check for models like qwen that support 1M context but
+ // don't have getModelCapability data (e.g. OpenAI-compatible providers)
+ if (modelSupports1M(model)) {
+ return 1_000_000
+ }
+
if (betas?.includes(CONTEXT_1M_BETA_HEADER) && modelSupports1M(model)) {
return 1_000_000
}