import type { Disposable } from 'vscode' import type { ClaudeProfile } from '../../cc/profiles' import { getDefaultProfilePath, getProfilesDir, listClaudeProfiles } from '../../cc/profiles' /** 下拉里的一项:id 是 profile 名;「默认」(不带 `--settings`,claude 用 ~/.claude 的登录)的 id 是 ''。 */ export interface ProfileOption { id: string label: string } export interface ProfileChoice { options: ProfileOption[] selected: string } export const DEFAULT_OPTION: ProfileOption = { id: '', label: '默认' } /** 下拉显示临时选择或设置默认值,失效的选择明确标出,避免回答时偷偷换模型。 */ export function profileChoice(profiles: readonly ClaudeProfile[], defaultPath: string, temporaryPath: string | undefined, configuredPath = ''): ProfileChoice { const fallback = profiles.find(profile => profile.path === defaultPath) const requestedPath = temporaryPath ?? (configuredPath || fallback?.path) const selected = profiles.find(profile => profile.path === requestedPath)?.name ?? requestedPath ?? DEFAULT_OPTION.id const options = [...(!fallback || selected === '' ? [DEFAULT_OPTION] : []), ...profiles.map(({ name }) => ({ id: name, label: name }))] if (!options.some(option => option.id === selected)) options.push({ id: selected, label: `不可用:${selected}` }) return { options, selected } } export interface ProfileSource { list: () => Promise defaultPath: () => string directory: () => string } const PROFILE_SOURCE: ProfileSource = { list: listClaudeProfiles, defaultPath: getDefaultProfilePath, directory: getProfilesDir } /** * 一套 profile 选择:临时切换所有预览共用、不写进设置,重新创建服务或这一套的默认配置改了以后回到设置里的默认值。 * * @param purpose 这一套给什么用(「问答」「重写注释」);报错时用它说是哪一套、该改设置里的哪一项 * @param configuredPath 读设置里这一套的默认 profile 路径 */ export class AskProfiles { private readonly listeners = new Set<(choice: ProfileChoice) => void>() private temporary: string | undefined private defaultKey: string | undefined constructor(readonly purpose: string, private readonly configuredPath: () => string, private readonly source: ProfileSource = PROFILE_SOURCE) {} async choice(): Promise { return (await this.read()).choice } /** 下一个问题使用当前显示的模型,失效时直接报错而不换成其他模型。 */ async current(): Promise { const { profiles, choice } = await this.read() this.publish(choice) const profile = profiles.find(profile => profile.name === choice.selected) if (choice.selected && !profile) throw new Error(`${this.purpose}模型不可用:${choice.selected}。请在设置的「${this.purpose}默认模型」里重新选择,或在输入框旁临时切换。`) return profile } /** 选择变化时通知所有打开的预览,让显示和实际发送使用同一个模型。 */ subscribe(listener: (choice: ProfileChoice) => void): Disposable { this.listeners.add(listener) return { dispose: () => this.listeners.delete(listener) } } async select(id: string): Promise { const { profiles, choice } = await this.read() const profile = profiles.find(profile => profile.name === id) if (id ? !profile : !choice.options.some(option => option.id === '')) throw new Error(`${this.purpose}模型不可用:${id}。请重新选择。`) this.temporary = profile?.path ?? '' await this.refresh() } async refresh(): Promise { this.publish(await this.choice()) } private publish(choice: ProfileChoice): void { for (const listener of [...this.listeners]) listener(choice) } private async read(): Promise<{ profiles: ClaudeProfile[], choice: ProfileChoice }> { const profiles = await this.source.list() const configuredPath = this.configuredPath().trim() const defaultPath = this.source.defaultPath() const key = JSON.stringify([configuredPath, this.source.directory()]) if (key !== this.defaultKey) { this.defaultKey = key this.temporary = undefined } return { profiles, choice: profileChoice(profiles, defaultPath, this.temporary, configuredPath) } } }