diff --git a/next.config.ts b/next.config.ts index 6c0eeb39..a9238548 100644 --- a/next.config.ts +++ b/next.config.ts @@ -9,6 +9,7 @@ const nextConfig: NextConfig = { serverExternalPackages: ['better-sqlite3', 'discord.js', '@discordjs/ws', 'zlib-sync'], env: { NEXT_PUBLIC_APP_VERSION: pkg.version, + NEXT_PUBLIC_SHOW_SOME: process.env.NEXT_PUBLIC_SHOW_SOME ?? 'true', }, }; diff --git a/package-lock.json b/package-lock.json index 1cd029cd..3f2597cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "@ai-sdk/google": "^3.0.31", "@ai-sdk/google-vertex": "^4.0.80", "@ai-sdk/openai": "^3.0.34", - "@anthropic-ai/claude-agent-sdk": "^0.2.62", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "@google/genai": "^1.43.0", "@larksuiteoapi/node-sdk": "^1.59.0", "@lobehub/icons": "^4.6.0", @@ -1461,9 +1461,9 @@ } }, "node_modules/@anthropic-ai/claude-agent-sdk": { - "version": "0.2.62", - "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.62.tgz", - "integrity": "sha512-exRJ2djKP6erRpUrtnVf5iXzqeS9dAie9d1ghODZVVXdLqieSqCpaAhZhe0hL1yvP33OL0J5CgT9RAyPzhYY9A==", + "version": "0.2.76", + "resolved": "https://registry.npmjs.org/@anthropic-ai/claude-agent-sdk/-/claude-agent-sdk-0.2.76.tgz", + "integrity": "sha512-HZxvnT8ZWkzCnQygaYCA0dl8RSUzuVbxE1YG4ecy6vh4nQbTT36CxUxBy+QVdR12pPQluncC0mCOLhI2918Eaw==", "license": "SEE LICENSE IN README.md", "engines": { "node": ">=18.0.0" @@ -15055,7 +15055,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, diff --git a/package.json b/package.json index 3e85a33d..372b5cc5 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "@ai-sdk/google": "^3.0.31", "@ai-sdk/google-vertex": "^4.0.80", "@ai-sdk/openai": "^3.0.34", - "@anthropic-ai/claude-agent-sdk": "^0.2.62", + "@anthropic-ai/claude-agent-sdk": "^0.2.76", "@google/genai": "^1.43.0", "@larksuiteoapi/node-sdk": "^1.59.0", "@lobehub/icons": "^4.6.0", diff --git a/src/app/api/providers/models/route.ts b/src/app/api/providers/models/route.ts index 761855fd..3b4af09e 100644 --- a/src/app/api/providers/models/route.ts +++ b/src/app/api/providers/models/route.ts @@ -9,6 +9,7 @@ import type { ErrorResponse, ProviderModelGroup } from '@/types'; const DEFAULT_MODELS = [ { value: 'sonnet', label: 'Sonnet 4.6' }, { value: 'opus', label: 'Opus 4.6' }, + { value: 'claude-opus-4-6[1m]', label: 'claude-opus-4-6[1m-c]' }, { value: 'haiku', label: 'Haiku 4.5' }, ]; @@ -69,7 +70,7 @@ export async function GET() { const { getCachedModels } = await import('@/lib/agent-sdk-capabilities'); const sdkModels = getCachedModels('env'); if (sdkModels.length > 0) { - groups[0].models = sdkModels.map(m => { + const mapped: Array & { value: string; label: string }> = sdkModels.map(m => { const cw = getContextWindow(m.value); return { value: m.value, @@ -81,6 +82,16 @@ export async function GET() { ...(cw != null ? { contextWindow: cw } : {}), }; }); + // Inject claude-opus-4-6[1m] if not already present from SDK + if (!mapped.some(m => m.value === 'claude-opus-4-6[1m]')) { + const cw = getContextWindow('claude-opus-4-6[1m]'); + mapped.push({ + value: 'claude-opus-4-6[1m]', + label: 'claude-opus-4-6[1m-c]', + ...(cw != null ? { contextWindow: cw } : {}), + }); + } + groups[0].models = mapped; } } catch { // SDK capabilities not available, keep defaults diff --git a/src/app/chat/page.tsx b/src/app/chat/page.tsx index f0ce6707..99d647f8 100644 --- a/src/app/chat/page.tsx +++ b/src/app/chat/page.tsx @@ -177,6 +177,25 @@ export default function NewChatPage() { localStorage.setItem('codepilot:last-working-directory', path); }, []); + const handleNewChat = useCallback(async () => { + const dir = workingDir || localStorage.getItem('codepilot:last-working-directory') || ''; + if (!dir) return; + try { + const lastModel = localStorage.getItem('codepilot:last-model') || currentModel; + const lastProvider = localStorage.getItem('codepilot:last-provider-id') || currentProviderId || ''; + const res = await fetch('/api/chat/sessions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ working_directory: dir, model: lastModel, provider_id: lastProvider }), + }); + if (res.ok) { + const data = await res.json(); + router.push(`/chat/${data.session.id}`); + window.dispatchEvent(new CustomEvent('session-created')); + } + } catch { /* ignore */ } + }, [workingDir, currentModel, currentProviderId, router]); + const stopStreaming = useCallback(() => { abortControllerRef.current?.abort(); abortControllerRef.current = null; @@ -503,13 +522,18 @@ export default function NewChatPage() { } }, [sendFirstMessage]); + const showSome = process.env.NEXT_PUBLIC_SHOW_SOME !== 'false'; + const showEmptyState = messages.length === 0 && !isStreaming && (!workingDir.trim() || !hasProvider); + const hideComposer = showEmptyState && !showSome && !hasProvider; + return (
- {messages.length === 0 && !isStreaming && (!workingDir.trim() || !hasProvider) ? ( + {showEmptyState ? ( @@ -525,49 +549,55 @@ export default function NewChatPage() { statusText={statusText} /> )} - {errorBanner && ( - setErrorBanner(null)} - actions={[ - { label: t('error.retry'), onClick: () => setErrorBanner(null) }, - ]} - /> - )} - - { - setCurrentProviderId(pid); - setCurrentModel(model); - }} - workingDirectory={workingDir} - effort={selectedEffort} - onEffortChange={setSelectedEffort} - /> - } - center={ - + {errorBanner && ( + setErrorBanner(null)} + actions={[ + { label: t('error.retry'), onClick: () => setErrorBanner(null) }, + ]} + /> + )} + - } - /> + { + setCurrentProviderId(pid); + setCurrentModel(model); + }} + workingDirectory={workingDir} + effort={selectedEffort} + onEffortChange={setSelectedEffort} + /> + {showSome && ( + } + center={ + + } + /> + )} + + )} void; + onNewChat?: () => void; recentProjects?: string[]; onSelectProject?: (path: string) => void; } @@ -16,12 +17,24 @@ export function ChatEmptyState({ hasDirectory, hasProvider, onSelectFolder, + onNewChat, recentProjects, onSelectProject, }: ChatEmptyStateProps) { const { t } = useTranslation(); + const showSome = process.env.NEXT_PUBLIC_SHOW_SOME !== 'false'; - if (hasDirectory && hasProvider) { + if (hasDirectory && (hasProvider || !showSome)) { + if (!showSome && onNewChat) { + return ( +
+ +
+ ); + } return (

{t('chat.empty.ready')}

@@ -66,7 +79,7 @@ export function ChatEmptyState({
)} - {!hasProvider && ( + {!hasProvider && showSome && (

{t('chat.empty.noProvider')}

)} + + {!hasProvider && !showSome && onNewChat && ( + + )}
); diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index 4870077b..bc98c0c0 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -437,22 +437,24 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal onEffortChange={setSelectedEffort} sdkInitMeta={initMetaRef.current} /> - } - center={ - - } - right={ - - } - /> + {process.env.NEXT_PUBLIC_SHOW_SOME !== 'false' && ( + } + center={ + + } + right={ + + } + /> + )} ); } diff --git a/src/components/chat/ModelSelectorDropdown.tsx b/src/components/chat/ModelSelectorDropdown.tsx index 83670f4f..036da9fa 100644 --- a/src/components/chat/ModelSelectorDropdown.tsx +++ b/src/components/chat/ModelSelectorDropdown.tsx @@ -132,12 +132,14 @@ export function ModelSelectorDropdown({ )} - - { setModelMenuOpen(false); setModelSearch(''); window.location.href = '/settings'; }}> - - {t('composer.manageProviders' as TranslationKey)} - - + {process.env.NEXT_PUBLIC_SHOW_SOME !== 'false' && ( + + { setModelMenuOpen(false); setModelSearch(''); window.location.href = '/settings'; }}> + + {t('composer.manageProviders' as TranslationKey)} + + + )} )} diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 3444e378..98353f8f 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -416,13 +416,15 @@ export function AppShell({ children }: { children: React.ReactNode }) {
- setChatListOpen(!chatListOpen)} - hasUpdate={updateContextValue.updateInfo?.updateAvailable ?? false} - readyToInstall={updateContextValue.updateInfo?.readyToInstall ?? false} - skipPermissionsActive={skipPermissionsActive} - /> + {process.env.NEXT_PUBLIC_SHOW_SOME !== 'false' && ( + setChatListOpen(!chatListOpen)} + hasUpdate={updateContextValue.updateInfo?.updateAvailable ?? false} + readyToInstall={updateContextValue.updateInfo?.readyToInstall ?? false} + skipPermissionsActive={skipPermissionsActive} + /> + )} diff --git a/src/components/layout/ChatListPanel.tsx b/src/components/layout/ChatListPanel.tsx index bb69314e..830ebfd6 100644 --- a/src/components/layout/ChatListPanel.tsx +++ b/src/components/layout/ChatListPanel.tsx @@ -392,17 +392,19 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) {
{/* Import CLI Session */} -
- -
+ {process.env.NEXT_PUBLIC_SHOW_SOME !== 'false' && ( +
+ +
+ )} {/* Session list grouped by project */} @@ -497,11 +499,13 @@ export function ChatListPanel({ open, width }: ChatListPanelProps) { {/* Version */} -
- - v{process.env.NEXT_PUBLIC_APP_VERSION} - -
+ {process.env.NEXT_PUBLIC_SHOW_SOME !== 'false' && ( +
+ + v{process.env.NEXT_PUBLIC_APP_VERSION} + +
+ )} {/* Import CLI Session Dialog */} ({ - locale: 'en', + locale: 'zh', setLocale: () => {}, t: (key) => key, }); export function I18nProvider({ children }: { children: ReactNode }) { - const [locale, setLocaleState] = useState('en'); + const [locale, setLocaleState] = useState('zh'); // Load persisted locale on mount useEffect(() => { diff --git a/src/components/layout/UnifiedTopBar.tsx b/src/components/layout/UnifiedTopBar.tsx index d5749549..c0426031 100644 --- a/src/components/layout/UnifiedTopBar.tsx +++ b/src/components/layout/UnifiedTopBar.tsx @@ -176,29 +176,31 @@ export function UnifiedTopBar() { > {isChatRoute && ( <> - - - - - {t('topBar.git')} - + {process.env.NEXT_PUBLIC_SHOW_SOME !== 'false' && ( + + + + + {t('topBar.git')} + + )} diff --git a/src/hooks/useProviderModels.ts b/src/hooks/useProviderModels.ts index 570d9460..efabf10c 100644 --- a/src/hooks/useProviderModels.ts +++ b/src/hooks/useProviderModels.ts @@ -5,6 +5,7 @@ import type { ProviderModelGroup } from '@/types'; export const DEFAULT_MODEL_OPTIONS = [ { value: 'sonnet', label: 'Sonnet 4.6' }, { value: 'opus', label: 'Opus 4.6' }, + { value: 'claude-opus-4-6[1m]', label: 'claude-opus-4-6[1m-c]' }, { value: 'haiku', label: 'Haiku 4.5' }, ]; diff --git a/src/lib/bridge/bridge-manager.ts b/src/lib/bridge/bridge-manager.ts index d3d8fbe7..2eea14d4 100644 --- a/src/lib/bridge/bridge-manager.ts +++ b/src/lib/bridge/bridge-manager.ts @@ -487,7 +487,9 @@ async function handleMessage( // Permission buttons const handled = broker.handlePermissionCallback(msg.callbackData, msg.address.chatId, msg.callbackMessageId); - if (handled) { + if (handled && !msg.callbackData.startsWith('ask:')) { + // Send confirmation for permission callbacks, but not for AskUserQuestion + // (AskUserQuestion selections are self-explanatory — no need for extra confirmation) const confirmMsg: OutboundMessage = { address: msg.address, text: 'Permission response recorded.', diff --git a/src/lib/bridge/permission-broker.ts b/src/lib/bridge/permission-broker.ts index 395a1245..588904a2 100644 --- a/src/lib/bridge/permission-broker.ts +++ b/src/lib/bridge/permission-broker.ts @@ -13,7 +13,7 @@ import type { PermissionUpdate } from '@anthropic-ai/claude-agent-sdk'; import type { ChannelAddress, OutboundMessage } from './types'; import type { BaseChannelAdapter } from './channel-adapter'; import { deliver } from './delivery-layer'; -import { insertPermissionLink, getPermissionLink, markPermissionLinkResolved, getSession, getDb } from '../db'; +import { insertPermissionLink, getPermissionLink, markPermissionLinkResolved, getSession, getDb, getPermissionRequest } from '../db'; import { resolvePendingPermission } from '../permission-registry'; import { escapeHtml } from './adapters/telegram-utils'; @@ -60,53 +60,60 @@ export async function forwardPermissionRequest( console.log(`[permission-broker] Forwarding permission request: ${permissionRequestId} tool=${toolName} channel=${adapter.channelType}`); - // Format the input summary (truncated) - const inputStr = JSON.stringify(toolInput, null, 2); - const truncatedInput = inputStr.length > 300 - ? inputStr.slice(0, 300) + '...' - : inputStr; - // Channels without inline button support (e.g. QQ) need text-based // permission commands. Check if the adapter ignores inlineButtons. const supportsButtons = adapter.channelType !== 'qq'; - const textLines = [ - `Permission Required`, - ``, - `Tool: ${escapeHtml(toolName)}`, - `
${escapeHtml(truncatedInput)}
`, - ``, - ]; + let message: OutboundMessage; - if (supportsButtons) { - textLines.push(`Choose an action:`); + // AskUserQuestion: render as interactive question form instead of raw JSON + if (toolName === 'AskUserQuestion' && supportsButtons) { + message = buildAskUserQuestionMessage(address, permissionRequestId, toolInput, replyToMessageId); } else { - // Text-based permission commands for channels without inline buttons - textLines.push( - `Reply with one of:`, - `/perm allow ${permissionRequestId}`, - `/perm allow_session ${permissionRequestId}`, - `/perm deny ${permissionRequestId}`, - ); - } - - const text = textLines.join('\n'); + // Generic permission card + const inputStr = JSON.stringify(toolInput, null, 2); + const truncatedInput = inputStr.length > 300 + ? inputStr.slice(0, 300) + '...' + : inputStr; + + const textLines = [ + `Permission Required`, + ``, + `Tool: ${escapeHtml(toolName)}`, + `
${escapeHtml(truncatedInput)}
`, + ``, + ]; + + if (supportsButtons) { + textLines.push(`Choose an action:`); + } else { + // Text-based permission commands for channels without inline buttons + textLines.push( + `Reply with one of:`, + `/perm allow ${permissionRequestId}`, + `/perm allow_session ${permissionRequestId}`, + `/perm deny ${permissionRequestId}`, + ); + } - const message: OutboundMessage = { - address, - text, - parseMode: supportsButtons ? 'HTML' : 'plain', - inlineButtons: supportsButtons - ? [ - [ - { text: 'Allow', callbackData: `perm:allow:${permissionRequestId}` }, - { text: 'Allow Session', callbackData: `perm:allow_session:${permissionRequestId}` }, - { text: 'Deny', callbackData: `perm:deny:${permissionRequestId}` }, - ], - ] - : undefined, - replyToMessageId, - }; + const text = textLines.join('\n'); + + message = { + address, + text, + parseMode: supportsButtons ? 'HTML' : 'plain', + inlineButtons: supportsButtons + ? [ + [ + { text: 'Allow', callbackData: `perm:allow:${permissionRequestId}` }, + { text: 'Allow Session', callbackData: `perm:allow_session:${permissionRequestId}` }, + { text: 'Deny', callbackData: `perm:deny:${permissionRequestId}` }, + ], + ] + : undefined, + replyToMessageId, + }; + } const result = await deliver(adapter, message, { sessionId }); @@ -126,66 +133,125 @@ export async function forwardPermissionRequest( } /** - * Handle a permission callback from an inline button press. - * Validates that the callback came from the same chat AND same message that - * received the permission request, prevents duplicate resolution via atomic - * DB check-and-set, and implements real allow_session semantics by passing - * updatedPermissions (suggestions). - * - * Returns true if the callback was recognized and handled. + * Build an OutboundMessage for AskUserQuestion with interactive option buttons. + * Each option becomes a button; clicking it selects and submits the answer. */ -export function handlePermissionCallback( - callbackData: string, - callbackChatId: string, - callbackMessageId?: string, -): boolean { - // Parse callback data: perm:action:permId - const parts = callbackData.split(':'); - if (parts.length < 3 || parts[0] !== 'perm') return false; +function buildAskUserQuestionMessage( + address: ChannelAddress, + permissionRequestId: string, + toolInput: Record, + replyToMessageId?: string, +): OutboundMessage { + const questions = (toolInput.questions || []) as Array<{ + question: string; + options: Array<{ label: string; description?: string }>; + multiSelect?: boolean; + header?: string; + }>; + + const textLines: string[] = []; + const buttons: { text: string; callbackData: string }[][] = []; + + for (let qIdx = 0; qIdx < questions.length; qIdx++) { + const q = questions[qIdx]; + if (q.header) { + textLines.push(`${escapeHtml(q.header)}`); + } + textLines.push(escapeHtml(q.question)); + textLines.push(''); + + // Each option as a button — callback: ask:{permId}:{qIdx}:{optionLabel} + const row: { text: string; callbackData: string }[] = []; + for (const opt of q.options) { + row.push({ + text: opt.label, + callbackData: `ask:${permissionRequestId}:${qIdx}:${opt.label}`, + }); + } + buttons.push(row); + } - const action = parts[1]; - const permissionRequestId = parts.slice(2).join(':'); // permId might contain colons + return { + address, + text: textLines.join('\n'), + parseMode: 'HTML', + inlineButtons: buttons, + replyToMessageId, + }; +} - // Look up the permission link to validate origin and check dedup +/** + * Validate and atomically claim a permission link for callback processing. + * Checks origin (chat/message ID), dedup (already resolved), and claims atomically. + * Returns the link on success, or null if validation/claim fails. + */ +function validateAndClaimLink( + permissionRequestId: string, + callbackChatId: string, + callbackMessageId?: string, +): ReturnType | null { const link = getPermissionLink(permissionRequestId); if (!link) { console.warn(`[permission-broker] No permission link found for ${permissionRequestId}`); - return false; + return null; } - // Security: verify the callback came from the same chat that received the request if (link.chatId !== callbackChatId) { - console.warn(`[permission-broker] Chat ID mismatch: expected ${link.chatId}, got ${callbackChatId}`); - return false; + console.warn(`[permission-broker] Chat ID mismatch for ${permissionRequestId}`); + return null; } - // Security: verify the callback came from the original permission message if (callbackMessageId && link.messageId !== callbackMessageId) { - console.warn(`[permission-broker] Message ID mismatch: expected ${link.messageId}, got ${callbackMessageId}`); - return false; + console.warn(`[permission-broker] Message ID mismatch for ${permissionRequestId}`); + return null; } - // Dedup: reject if already resolved (fast path before expensive resolution) if (link.resolved) { console.warn(`[permission-broker] Permission ${permissionRequestId} already resolved`); - return false; + return null; } - // Atomically mark as resolved BEFORE calling resolvePendingPermission - // to prevent race conditions with concurrent button clicks - let claimed: boolean; try { - claimed = markPermissionLinkResolved(permissionRequestId); + if (!markPermissionLinkResolved(permissionRequestId)) { + console.warn(`[permission-broker] Permission ${permissionRequestId} already claimed by concurrent handler`); + return null; + } } catch { - return false; + return null; } - if (!claimed) { - // Another concurrent handler already resolved this permission - console.warn(`[permission-broker] Permission ${permissionRequestId} already claimed by concurrent handler`); - return false; + return link; +} + +/** + * Handle a permission callback from an inline button press. + * Validates that the callback came from the same chat AND same message that + * received the permission request, prevents duplicate resolution via atomic + * DB check-and-set, and implements real allow_session semantics by passing + * updatedPermissions (suggestions). + * + * Returns true if the callback was recognized and handled. + */ +export function handlePermissionCallback( + callbackData: string, + callbackChatId: string, + callbackMessageId?: string, +): boolean { + // Handle AskUserQuestion callbacks: ask:{permId}:{qIdx}:{optionLabel} + if (callbackData.startsWith('ask:')) { + return handleAskUserQuestionCallback(callbackData, callbackChatId, callbackMessageId); } + // Parse callback data: perm:action:permId + const parts = callbackData.split(':'); + if (parts.length < 3 || parts[0] !== 'perm') return false; + + const action = parts[1]; + const permissionRequestId = parts.slice(2).join(':'); + + const link = validateAndClaimLink(permissionRequestId, callbackChatId, callbackMessageId); + if (!link) return false; + let resolved: boolean; switch (action) { @@ -196,7 +262,6 @@ export function handlePermissionCallback( break; case 'allow_session': { - // Parse stored suggestions so subsequent same-tool calls auto-approve let updatedPermissions: PermissionUpdate[] | undefined; if (link.suggestions) { try { @@ -225,6 +290,50 @@ export function handlePermissionCallback( return resolved; } +/** + * Handle AskUserQuestion callback: resolve with the selected option as answer. + * Callback format: ask:{permId}:{qIdx}:{optionLabel} + */ +function handleAskUserQuestionCallback( + callbackData: string, + callbackChatId: string, + callbackMessageId?: string, +): boolean { + // Parse: ask:{permId}:{qIdx}:{optionLabel} + // Strategy: split from the end — last segment is optionLabel, second-to-last is qIdx, rest is permId + const segments = callbackData.slice(4).split(':'); // skip "ask:" + if (segments.length < 3) return false; + + const optionLabel = segments[segments.length - 1]; + const qIdxStr = segments[segments.length - 2]; + const permissionRequestId = segments.slice(0, -2).join(':'); + const qIdx = parseInt(qIdxStr, 10); + if (isNaN(qIdx)) return false; + + if (!validateAndClaimLink(permissionRequestId, callbackChatId, callbackMessageId)) return false; + + // Build updatedInput matching the format the PC UI sends: + // { questions: originalQuestions, answers: { [question]: selectedOption } } + const permRow = getPermissionRequest(permissionRequestId); + let originalQuestions: Array<{ question: string; options: unknown[]; multiSelect?: boolean; header?: string }> = []; + if (permRow?.tool_input) { + try { + const toolInput = JSON.parse(permRow.tool_input); + originalQuestions = toolInput.questions || []; + } catch { /* fallback */ } + } + + const answers: Record = {}; + if (originalQuestions[qIdx]) { + answers[originalQuestions[qIdx].question] = optionLabel; + } + + return resolvePendingPermission(permissionRequestId, { + behavior: 'allow', + updatedInput: { questions: originalQuestions, answers }, + }); +} + /** * Auto-approve all pending permission requests for a session. * Called when a session switches from 'default' to 'full_access' profile. diff --git a/src/lib/channels/feishu/outbound.ts b/src/lib/channels/feishu/outbound.ts index 1026cfae..db89b62b 100644 --- a/src/lib/channels/feishu/outbound.ts +++ b/src/lib/channels/feishu/outbound.ts @@ -179,18 +179,21 @@ async function sendAsInteractiveCard( const firstCallback = inlineButtons[0]?.[0]?.callbackData || ''; const isPermission = firstCallback.startsWith('perm:'); const isCwd = firstCallback.startsWith('cwd:'); + const isAskQuestion = firstCallback.startsWith('ask:'); // Build button elements const allButtons = inlineButtons.flat(); const buttonColumns = allButtons.map((btn) => { let btnType: 'primary' | 'danger' | 'default' = 'default'; - const lowerText = btn.text.toLowerCase(); - if (lowerText.includes('deny') || lowerText.includes('拒绝')) { - btnType = 'danger'; - } else if (lowerText.includes('allow') || lowerText.includes('允许')) { - btnType = 'primary'; - } else if (btn.text.startsWith('📍')) { - btnType = 'primary'; // Current project highlighted + if (!isAskQuestion) { + const lowerText = btn.text.toLowerCase(); + if (lowerText.includes('deny') || lowerText.includes('拒绝')) { + btnType = 'danger'; + } else if (lowerText.includes('allow') || lowerText.includes('允许')) { + btnType = 'primary'; + } else if (btn.text.startsWith('📍')) { + btnType = 'primary'; // Current project highlighted + } } return { @@ -210,11 +213,13 @@ async function sendAsInteractiveCard( }); // Card header based on type - const headerConfig = isPermission - ? { title: 'Permission Required', template: 'blue' as const, icon: 'lock-chat_filled' } - : isCwd - ? { title: 'Switch Project', template: 'turquoise' as const, icon: 'folder_outlined' } - : { title: 'Action Required', template: 'blue' as const, icon: 'info-circle_outlined' }; + const headerConfig = isAskQuestion + ? { title: 'Question', template: 'indigo' as const, icon: 'info-circle_outlined' } + : isPermission + ? { title: 'Permission Required', template: 'blue' as const, icon: 'lock-chat_filled' } + : isCwd + ? { title: 'Switch Project', template: 'turquoise' as const, icon: 'folder_outlined' } + : { title: 'Action Required', template: 'blue' as const, icon: 'info-circle_outlined' }; // Build body elements const bodyElements: any[] = [ @@ -244,6 +249,14 @@ async function sendAsInteractiveCard( columns: [col], }); } + } else if (isAskQuestion) { + // AskUserQuestion: use flow layout so option buttons wrap horizontally + bodyElements.push({ + tag: 'column_set' as const, + flex_mode: 'flow' as const, + horizontal_align: 'left' as const, + columns: buttonColumns, + }); } else { bodyElements.push({ tag: 'column_set' as const, diff --git a/src/lib/model-context.ts b/src/lib/model-context.ts index 828c0c5a..41d74a27 100644 --- a/src/lib/model-context.ts +++ b/src/lib/model-context.ts @@ -1,6 +1,7 @@ export const MODEL_CONTEXT_WINDOWS: Record = { 'sonnet': 200000, 'opus': 200000, + 'claude-opus-4-6[1m]': 1000000, 'haiku': 200000, 'claude-sonnet-4-20250514': 200000, 'claude-opus-4-20250514': 200000, diff --git a/src/lib/provider-catalog.ts b/src/lib/provider-catalog.ts index e8d97fa0..a79bd3fd 100644 --- a/src/lib/provider-catalog.ts +++ b/src/lib/provider-catalog.ts @@ -116,6 +116,7 @@ export interface VendorPreset { const ANTHROPIC_DEFAULT_MODELS: CatalogModel[] = [ { modelId: 'sonnet', displayName: 'Sonnet 4.6', role: 'sonnet' }, { modelId: 'opus', displayName: 'Opus 4.6', role: 'opus' }, + { modelId: 'claude-opus-4-6[1m]', displayName: 'claude-opus-4-6[1m-c]', role: 'opus' }, { modelId: 'haiku', displayName: 'Haiku 4.5', role: 'haiku' }, ]; diff --git a/src/lib/provider-resolver.ts b/src/lib/provider-resolver.ts index 0e59fa80..8658ea9e 100644 --- a/src/lib/provider-resolver.ts +++ b/src/lib/provider-resolver.ts @@ -464,6 +464,7 @@ function buildResolution( const envModels: CatalogModel[] = [ { modelId: 'sonnet', upstreamModelId: 'claude-sonnet-4-20250514', displayName: 'Sonnet 4.6' }, { modelId: 'opus', upstreamModelId: 'claude-opus-4-20250514', displayName: 'Opus 4.6' }, + { modelId: 'claude-opus-4-6[1m]', upstreamModelId: 'claude-opus-4-6[1m]', displayName: 'claude-opus-4-6[1m-c]' }, { modelId: 'haiku', upstreamModelId: 'claude-haiku-4-5-20251001', displayName: 'Haiku 4.5' }, ];