Files
superwork/vscode/src/codeReader/qa/askProfiles.ts
T

100 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<ClaudeProfile[]>
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<ProfileChoice> {
return (await this.read()).choice
}
/** 下一个问题使用当前显示的模型,失效时直接报错而不换成其他模型。 */
async current(): Promise<ClaudeProfile | undefined> {
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<void> {
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<void> {
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) }
}
}