🐛 fix(vscode): 修复看板缓存并增加问答默认模型
This commit is contained in:
1 parent
c38f1dc468
commit
012daf95b5
25 files changed
+1753
-164
No files matched your search
@@ -1,9 +1,6 @@
|
||||
import type { Disposable, Memento } from 'vscode'
|
||||
import type { Disposable } from 'vscode'
|
||||
import type { ClaudeProfile } from '../../cc/profiles'
|
||||
import { getDefaultProfilePath, listClaudeProfiles } from '../../cc/profiles'
|
||||
|
||||
/** globalState 里记的「问 AI」最近选的 profile 名(「默认」记成 '')。 */
|
||||
export const ASK_PROFILE_KEY = 'superpowers.codeReader.askProfile'
|
||||
import { getDefaultProfilePath, getProfilesDir, listClaudeProfiles } from '../../cc/profiles'
|
||||
|
||||
/** 下拉里的一项:id 是 profile 名;「默认」(不带 `--settings`,claude 用 ~/.claude 的登录)的 id 是 ''。 */
|
||||
export interface ProfileOption {
|
||||
@@ -18,60 +15,80 @@ export interface ProfileChoice {
|
||||
|
||||
export const DEFAULT_OPTION: ProfileOption = { id: '', label: '默认' }
|
||||
|
||||
/**
|
||||
* 下拉的选项和选中的那项。插件的默认 profile(getDefaultProfilePath)在列表里时,没选过就选它;
|
||||
* 不在列表里(一个 profile 都没有,或没有 offical.json)时最前面加一项「默认」,没选过就选它。
|
||||
* 记着的那项不在列表里了(profile 删了、目录换了)也回到这一项。
|
||||
*/
|
||||
export function profileChoice(profiles: readonly ClaudeProfile[], defaultPath: string, remembered: unknown): ProfileChoice {
|
||||
/** 下拉显示临时选择或设置默认值,失效的选择明确标出,避免回答时偷偷换模型。 */
|
||||
export function profileChoice(profiles: readonly ClaudeProfile[], defaultPath: string, temporaryPath: string | undefined, configuredPath = ''): ProfileChoice {
|
||||
const fallback = profiles.find(profile => profile.path === defaultPath)
|
||||
const options = [...(fallback ? [] : [DEFAULT_OPTION]), ...profiles.map(({ name }) => ({ id: name, label: name }))]
|
||||
const selected = options.find(option => option.id === remembered)?.id ?? fallback?.name ?? DEFAULT_OPTION.id
|
||||
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 }
|
||||
const PROFILE_SOURCE: ProfileSource = { list: listClaudeProfiles, defaultPath: getDefaultProfilePath, directory: getProfilesDir }
|
||||
|
||||
/**
|
||||
* 「问 AI」用哪个 Claude Code profile:所有预览共用一份,记在 globalState 里,换个预览、重启 VS Code 都还是它;
|
||||
* 一个预览改了,所有订阅的预览都收到(包括改的那个,webview 只按扩展端发来的显示)。
|
||||
* profile 列表每次现读(同看板:cc/profiles 的目录),不依赖 vscode 运行时,便于单元测试。
|
||||
*/
|
||||
/** 所有预览共用临时模型选择,重新创建服务或修改默认配置后恢复设置里的默认值。 */
|
||||
export class AskProfiles {
|
||||
private readonly listeners = new Set<(choice: ProfileChoice) => void>()
|
||||
private temporary: string | undefined
|
||||
private defaultKey: string | undefined
|
||||
|
||||
constructor(private readonly state: Pick<Memento, 'get' | 'update'>, private readonly source: ProfileSource = PROFILE_SOURCE) {}
|
||||
constructor(private readonly configuredPath: () => string, private readonly source: ProfileSource = PROFILE_SOURCE) {}
|
||||
|
||||
async choice(): Promise<ProfileChoice> {
|
||||
return (await this.read()).choice
|
||||
}
|
||||
|
||||
/** 下一个问题用的 profile;选的是「默认」时没有(不带 `--settings`)。 */
|
||||
/** 下一个问题使用当前显示的模型,失效时直接报错而不换成其他模型。 */
|
||||
async current(): Promise<ClaudeProfile | undefined> {
|
||||
const { profiles, choice } = await this.read()
|
||||
return profiles.find(profile => profile.name === choice.selected)
|
||||
this.publish(choice)
|
||||
const profile = profiles.find(profile => profile.name === choice.selected)
|
||||
if (choice.selected && !profile)
|
||||
throw new Error(`文件问答模型不可用:${choice.selected}。请在设置中重新选择默认模型,或在输入框旁临时切换。`)
|
||||
return profile
|
||||
}
|
||||
|
||||
/** 选了就调 listener,带上新的选项和选中的那项。 */
|
||||
/** 选择变化时通知所有打开的预览,让显示和实际发送使用同一个模型。 */
|
||||
subscribe(listener: (choice: ProfileChoice) => void): Disposable {
|
||||
this.listeners.add(listener)
|
||||
return { dispose: () => this.listeners.delete(listener) }
|
||||
}
|
||||
|
||||
async select(id: string): Promise<void> {
|
||||
await this.state.update(ASK_PROFILE_KEY, id)
|
||||
const choice = await this.choice()
|
||||
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(`文件问答模型不可用:${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()
|
||||
return { profiles, choice: profileChoice(profiles, this.source.defaultPath(), this.state.get(ASK_PROFILE_KEY)) }
|
||||
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) }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user