✨ feat(vscode): 问 AI 和重写注释各用各的 profile,设置里加重写注释默认模型
This commit is contained in:
1 parent
925f1ed39a
commit
b83c445dd0
25 files changed
+361
-109
No files matched your search
@@ -107,6 +107,9 @@ export interface QaMessage {
|
|||||||
rewriteCurrent: AskRound[]
|
rewriteCurrent: AskRound[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 预览里的两种对话:问 AI(ask,块下面和页面顶部)、重写注释(rewrite),各有各的记录和 profile 下拉 */
|
||||||
|
export type ChatKind = 'ask' | 'rewrite'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* VS Code 的 Sticky Scroll 设置(editor.stickyScroll.enabled / editor.stickyScroll.maxLineCount):卡片的开头滚到预览顶上后面、卡片还有一部分看得见时把它钉在顶上,
|
* VS Code 的 Sticky Scroll 设置(editor.stickyScroll.enabled / editor.stickyScroll.maxLineCount):卡片的开头滚到预览顶上后面、卡片还有一部分看得见时把它钉在顶上,
|
||||||
* 外层的在上、里层的在下,最多钉 maxLineCount 行。
|
* 外层的在上、里层的在下,最多钉 maxLineCount 行。
|
||||||
@@ -126,7 +129,7 @@ export interface StickyScrollSettings {
|
|||||||
* occurrencesFound:id 那次 findOccurrences 的回答,语言扩展没答(或没装)就是空的
|
* occurrencesFound:id 那次 findOccurrences 的回答,语言扩展没答(或没装)就是空的
|
||||||
* filter:整个工作区共用的块筛选和「只看改动」开关;ready 后先于第一份文档发一次,之后任何一个预览改了都发给所有预览
|
* filter:整个工作区共用的块筛选和「只看改动」开关;ready 后先于第一份文档发一次,之后任何一个预览改了都发给所有预览
|
||||||
* qa:这份文件的问答;ready 后发一次,问答、正在答的问题变了再发
|
* qa:这份文件的问答;ready 后发一次,问答、正在答的问题变了再发
|
||||||
* askProfiles:「问 AI」输入框旁边的 profile 下拉:选项和选中的那项;ready 后发一次,任何一个预览改了都发给所有预览
|
* profiles:kind 那种对话输入框旁边的 profile 下拉:选项和选中的那项;ready 后两种各发一次,任何一个预览改了哪种就把哪种发给所有预览
|
||||||
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
|
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
|
||||||
* answerEnd:askId 那个问题答完了:done 带最终的回答和存下的那条的 id,failed 带给人看的原因,stopped 是被叫停
|
* answerEnd:askId 那个问题答完了:done 带最终的回答和存下的那条的 id,failed 带给人看的原因,stopped 是被叫停
|
||||||
* rewriteProgress:askId 那一轮重写排上队了(queued)、开始了、正文长了(text 是到现在为止写出来的)、或换了在做的事;question 是记在这一轮上的话(点按钮时是那句技能命令)
|
* rewriteProgress:askId 那一轮重写排上队了(queued)、开始了、正文长了(text 是到现在为止写出来的)、或换了在做的事;question 是记在这一轮上的话(点按钮时是那句技能命令)
|
||||||
@@ -157,7 +160,7 @@ export type ExtensionToWebview
|
|||||||
| { type: 'displayNames', version: number, names: ShownName[] }
|
| { type: 'displayNames', version: number, names: ShownName[] }
|
||||||
| ({ type: 'filter' } & FilterState)
|
| ({ type: 'filter' } & FilterState)
|
||||||
| QaMessage
|
| QaMessage
|
||||||
| ({ type: 'askProfiles' } & ProfileChoice)
|
| ({ type: 'profiles', kind: ChatKind } & ProfileChoice)
|
||||||
| { type: 'answer', askId: string, text: string, activity?: string }
|
| { type: 'answer', askId: string, text: string, activity?: string }
|
||||||
| { type: 'answerEnd', askId: string, status: 'done' | 'failed' | 'stopped', text?: string, error?: string, entryId?: string }
|
| { type: 'answerEnd', askId: string, status: 'done' | 'failed' | 'stopped', text?: string, error?: string, entryId?: string }
|
||||||
| { type: 'rewriteProgress', askId: string, question: string, text: string, activity?: string, queued?: true }
|
| { type: 'rewriteProgress', askId: string, question: string, text: string, activity?: string, queued?: true }
|
||||||
@@ -184,7 +187,7 @@ export type ExtensionToWebview
|
|||||||
* findOccurrences:单击带下标的名字,问它在这份文档里出现在哪几处;id、version 同 probeDefinition
|
* findOccurrences:单击带下标的名字,问它在这份文档里出现在哪几处;id、version 同 probeDefinition
|
||||||
* setFilter:改块筛选(整份替换),扩展端记下后发 filter 给所有预览
|
* setFilter:改块筛选(整份替换),扩展端记下后发 filter 给所有预览
|
||||||
* setOnlyChanges:打开 / 关掉「只看改动」,扩展端记下后发 filter 给所有预览
|
* setOnlyChanges:打开 / 关掉「只看改动」,扩展端记下后发 filter 给所有预览
|
||||||
* ask:问 anchor 那块代码;askId 由 webview 起,回答的消息带回去;扩展端接着这块最近一次问答的会话问,用眼下选的 profile;
|
* ask:问 anchor 那块代码;askId 由 webview 起,回答的消息带回去;扩展端接着这块最近一次问答的会话问,用问 AI 眼下选的 profile;
|
||||||
* images 是贴进输入框的图片(没贴是空的),随问题发给 Claude Code,答成了跟这条一起存
|
* images 是贴进输入框的图片(没贴是空的),随问题发给 Claude Code,答成了跟这条一起存
|
||||||
* rewriteComment:按注释规范重写 anchor 那块的注释(还没有就新写一段);askId 由 webview 起,过程用 rewriteProgress / rewriteEnd 带回去。
|
* rewriteComment:按注释规范重写 anchor 那块的注释(还没有就新写一段);askId 由 webview 起,过程用 rewriteProgress / rewriteEnd 带回去。
|
||||||
* 没有 request 是点了按钮:发技能命令,开新会话;有 request 是在输入框里说的修改意见,接着这块最近一次重写的会话说,images 是贴的图片
|
* 没有 request 是点了按钮:发技能命令,开新会话;有 request 是在输入框里说的修改意见,接着这块最近一次重写的会话说,images 是贴的图片
|
||||||
@@ -193,7 +196,7 @@ export type ExtensionToWebview
|
|||||||
* deleteRewrite:删掉 anchor 那块下面的一条重写记录
|
* deleteRewrite:删掉 anchor 那块下面的一条重写记录
|
||||||
* stopAsk:叫停 askId 那个问题,这次不存
|
* stopAsk:叫停 askId 那个问题,这次不存
|
||||||
* deleteQa:删掉 anchor 那块下面的一次问答(已经理解了)
|
* deleteQa:删掉 anchor 那块下面的一次问答(已经理解了)
|
||||||
* selectAskProfile:「问 AI」换 profile(id 是 profile 名,「默认」是 ''),扩展端记下后发 askProfiles 给所有预览
|
* selectProfile:kind 那种对话换 profile(id 是 profile 名,「默认」是 ''),扩展端记下后把这种的 profiles 发给所有预览
|
||||||
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发;
|
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发;
|
||||||
* 扩展端按它设 when 子句用的上下文键
|
* 扩展端按它设 when 子句用的上下文键
|
||||||
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
||||||
@@ -211,7 +214,7 @@ export type WebviewToExtension
|
|||||||
| { type: 'rewriteComment', askId: string, anchor: string, request?: string, images: ClaudeImage[] }
|
| { type: 'rewriteComment', askId: string, anchor: string, request?: string, images: ClaudeImage[] }
|
||||||
| { type: 'stopRewrite', askId: string }
|
| { type: 'stopRewrite', askId: string }
|
||||||
| { type: 'deleteRewrite', anchor: string, entryId: string }
|
| { type: 'deleteRewrite', anchor: string, entryId: string }
|
||||||
| { type: 'forgetRound', kind: 'ask' | 'rewrite', anchor: string }
|
| { type: 'forgetRound', kind: ChatKind, anchor: string }
|
||||||
| { type: 'selectTranslator', id: string }
|
| { type: 'selectTranslator', id: string }
|
||||||
| { type: 'visibleLine', line: number }
|
| { type: 'visibleLine', line: number }
|
||||||
| { type: 'probeDefinition', id: number, version: number, offset: number }
|
| { type: 'probeDefinition', id: number, version: number, offset: number }
|
||||||
@@ -222,7 +225,7 @@ export type WebviewToExtension
|
|||||||
| { type: 'ask', askId: string, anchor: string, question: string, images: ClaudeImage[] }
|
| { type: 'ask', askId: string, anchor: string, question: string, images: ClaudeImage[] }
|
||||||
| { type: 'stopAsk', askId: string }
|
| { type: 'stopAsk', askId: string }
|
||||||
| { type: 'deleteQa', anchor: string, entryId: string }
|
| { type: 'deleteQa', anchor: string, entryId: string }
|
||||||
| { type: 'selectAskProfile', id: string }
|
| { type: 'selectProfile', kind: ChatKind, id: string }
|
||||||
| { type: 'occurrenceNavigation', available: boolean }
|
| { type: 'occurrenceNavigation', available: boolean }
|
||||||
| { type: 'openRepoLink', path?: string, symbol?: string }
|
| { type: 'openRepoLink', path?: string, symbol?: string }
|
||||||
| { type: 'checkRepoLinks', id: number, paths: string[], symbols: string[] }
|
| { type: 'checkRepoLinks', id: number, paths: string[], symbols: string[] }
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import type { DiffBase, OpenedFrom } from '../diff/diffBase'
|
import type { DiffBase, OpenedFrom } from '../diff/diffBase'
|
||||||
|
import type { ProfileChoice } from '../qa/askProfiles'
|
||||||
import type { RewriteRequest } from '../qa/codeQa'
|
import type { RewriteRequest } from '../qa/codeQa'
|
||||||
import type { ExtensionToWebview, WebviewToExtension } from './messages'
|
import type { ExtensionToWebview, WebviewToExtension } from './messages'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
import { workspace } from 'vscode'
|
import { window, workspace } from 'vscode'
|
||||||
import { PreviewEditorProvider } from './previewEditor'
|
import { PreviewEditorProvider } from './previewEditor'
|
||||||
|
|
||||||
vi.mock('vscode', () => ({
|
vi.mock('vscode', () => ({
|
||||||
@@ -49,7 +50,7 @@ function preview(rewrite: (uri: unknown, request: RewriteRequest) => Promise<str
|
|||||||
const uri = { fsPath: '/workspace/web/a.ts', scheme: 'file', toString: () => 'file:///workspace/web/a.ts' }
|
const uri = { fsPath: '/workspace/web/a.ts', scheme: 'file', toString: () => 'file:///workspace/web/a.ts' }
|
||||||
const document = { uri, isDirty: dirty, lineAt: (line: number) => ({ text: SOURCE[line] }), lineCount: SOURCE.length, eol: 1 }
|
const document = { uri, isDirty: dirty, lineAt: (line: number) => ({ text: SOURCE[line] }), lineCount: SOURCE.length, eol: 1 }
|
||||||
vi.mocked(workspace.openTextDocument).mockResolvedValue(document as never)
|
vi.mocked(workspace.openTextDocument).mockResolvedValue(document as never)
|
||||||
const qa = { imageRoot: () => undefined, onDidChange: subscribe, onDidAnswer: subscribe, onDidRewrite: subscribe, profiles: { subscribe }, rewrite: vi.fn(rewrite) }
|
const qa = { imageRoot: () => undefined, onDidChange: subscribe, onDidAnswer: subscribe, onDidRewrite: subscribe, profiles: { ask: { subscribe }, rewrite: { subscribe } }, rewrite: vi.fn(rewrite) }
|
||||||
const provider = new PreviewEditorProvider(...[
|
const provider = new PreviewEditorProvider(...[
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
@@ -75,7 +76,7 @@ function preview(rewrite: (uri: unknown, request: RewriteRequest) => Promise<str
|
|||||||
|
|
||||||
describe('预览里重写注释', () => {
|
describe('预览里重写注释', () => {
|
||||||
it('点按钮:发技能命令、开新会话,记在这一轮上的就是这句命令;开不了时回一条带 askId 的失败,能再试', async () => {
|
it('点按钮:发技能命令、开新会话,记在这一轮上的就是这句命令;开不了时回一条带 askId 的失败,能再试', async () => {
|
||||||
const error = '文件问答模型不可用:official。请重新选择。'
|
const error = '重写注释模型不可用:official。请重新选择。'
|
||||||
const editor = preview(async () => error)
|
const editor = preview(async () => error)
|
||||||
try {
|
try {
|
||||||
await editor.send({ type: 'rewriteComment', askId: 'r1', anchor: 'block', images: [] })
|
await editor.send({ type: 'rewriteComment', askId: 'r1', anchor: 'block', images: [] })
|
||||||
@@ -139,8 +140,24 @@ describe('预览里重写注释', () => {
|
|||||||
|
|
||||||
const HEAD_BASE: DiffBase = { label: '上一次提交', key: 'head:web/a.ts', read: async () => '// 改之前', missing: '' }
|
const HEAD_BASE: DiffBase = { label: '上一次提交', key: 'head:web/a.ts', read: async () => '// 改之前', missing: '' }
|
||||||
|
|
||||||
|
const OPTIONS = [{ id: 'offical', label: 'offical' }, { id: 'sonnet', label: 'sonnet' }, { id: 'dsflash', label: 'dsflash' }]
|
||||||
|
|
||||||
|
/** 一套 profile 选择:眼下选的是 selected,publish 模拟它变了、通知订阅的预览 */
|
||||||
|
function profileSet(selected: string) {
|
||||||
|
const listeners = new Set<(choice: ProfileChoice) => void>()
|
||||||
|
return {
|
||||||
|
subscribe: (listener: (choice: ProfileChoice) => void) => {
|
||||||
|
listeners.add(listener)
|
||||||
|
return { dispose: () => listeners.delete(listener) }
|
||||||
|
},
|
||||||
|
choice: async (): Promise<ProfileChoice> => ({ options: OPTIONS, selected }),
|
||||||
|
select: vi.fn(async (_id: string) => {}),
|
||||||
|
publish: (choice: ProfileChoice) => listeners.forEach(listener => listener(choice)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** 对比基准只认从「未提交的更改」点开的:从那儿点开的对比上一次提交,别处打开的不比 */
|
/** 对比基准只认从「未提交的更改」点开的:从那儿点开的对比上一次提交,别处打开的不比 */
|
||||||
function opening() {
|
function opening(profiles = { ask: profileSet(''), rewrite: profileSet('') }) {
|
||||||
const subscribe = () => ({ dispose: () => {} })
|
const subscribe = () => ({ dispose: () => {} })
|
||||||
const diffBase = vi.fn((_uri: unknown, from: OpenedFrom): DiffBase | undefined => from === 'uncommitted' ? HEAD_BASE : undefined)
|
const diffBase = vi.fn((_uri: unknown, from: OpenedFrom): DiffBase | undefined => from === 'uncommitted' ? HEAD_BASE : undefined)
|
||||||
const qa = {
|
const qa = {
|
||||||
@@ -148,7 +165,7 @@ function opening() {
|
|||||||
onDidChange: subscribe,
|
onDidChange: subscribe,
|
||||||
onDidAnswer: subscribe,
|
onDidAnswer: subscribe,
|
||||||
onDidRewrite: subscribe,
|
onDidRewrite: subscribe,
|
||||||
profiles: { subscribe, choice: async () => ({ profiles: [], selected: '' }) },
|
profiles,
|
||||||
view: () => ({ type: 'qa' }),
|
view: () => ({ type: 'qa' }),
|
||||||
}
|
}
|
||||||
const provider = new PreviewEditorProvider(...[
|
const provider = new PreviewEditorProvider(...[
|
||||||
@@ -290,3 +307,41 @@ describe('预览对比什么看这个标签页是从哪儿打开的', () => {
|
|||||||
expect(diffBase).not.toHaveBeenCalledWith(document.uri, 'uncommitted')
|
expect(diffBase).not.toHaveBeenCalledWith(document.uri, 'uncommitted')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('问 AI 和重写注释的 profile 下拉各管各的', () => {
|
||||||
|
it('ready 后两套各发一次;哪一套变了只发哪一套', async () => {
|
||||||
|
const profiles = { ask: profileSet('sonnet'), rewrite: profileSet('dsflash') }
|
||||||
|
const { provider, document } = opening(profiles)
|
||||||
|
const page = tab()
|
||||||
|
provider.resolveCustomTextEditor(document as never, page.panel)
|
||||||
|
await page.send({ type: 'ready' })
|
||||||
|
await vi.waitFor(() => {
|
||||||
|
expect(page.posted()).toContainEqual({ type: 'profiles', kind: 'ask', options: OPTIONS, selected: 'sonnet' })
|
||||||
|
expect(page.posted()).toContainEqual({ type: 'profiles', kind: 'rewrite', options: OPTIONS, selected: 'dsflash' })
|
||||||
|
})
|
||||||
|
|
||||||
|
page.clear()
|
||||||
|
profiles.rewrite.publish({ options: OPTIONS, selected: 'offical' })
|
||||||
|
expect(page.posted()).toContainEqual({ type: 'profiles', kind: 'rewrite', options: OPTIONS, selected: 'offical' })
|
||||||
|
expect(page.posted()).not.toContainEqual(expect.objectContaining({ type: 'profiles', kind: 'ask' }))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('下拉换了哪一套就只改哪一套;换不成时报错,并把那一套眼下的选择发回去', async () => {
|
||||||
|
const profiles = { ask: profileSet('sonnet'), rewrite: profileSet('dsflash') }
|
||||||
|
const { provider, document } = opening(profiles)
|
||||||
|
const page = tab()
|
||||||
|
provider.resolveCustomTextEditor(document as never, page.panel)
|
||||||
|
await page.send({ type: 'ready' })
|
||||||
|
|
||||||
|
await page.send({ type: 'selectProfile', kind: 'rewrite', id: 'offical' })
|
||||||
|
expect(profiles.rewrite.select).toHaveBeenCalledWith('offical')
|
||||||
|
expect(profiles.ask.select).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
profiles.ask.select.mockRejectedValueOnce(new Error('问答模型不可用:gone。请重新选择。'))
|
||||||
|
page.clear()
|
||||||
|
await page.send({ type: 'selectProfile', kind: 'ask', id: 'gone' })
|
||||||
|
expect(vi.mocked(window.showErrorMessage)).toHaveBeenCalledWith('问答模型不可用:gone。请重新选择。')
|
||||||
|
expect(page.posted()).toContainEqual({ type: 'profiles', kind: 'ask', options: OPTIONS, selected: 'sonnet' })
|
||||||
|
expect(page.posted()).not.toContainEqual(expect.objectContaining({ type: 'profiles', kind: 'rewrite' }))
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -14,7 +14,7 @@ import type { TreeSitterRuntime } from '../runtime'
|
|||||||
import type { ImportedName, LinkTarget, SymbolNode } from '../symbolLinks'
|
import type { ImportedName, LinkTarget, SymbolNode } from '../symbolLinks'
|
||||||
import type { BlockFilter } from './blockFilter'
|
import type { BlockFilter } from './blockFilter'
|
||||||
import type { FoundLocation } from './definitions'
|
import type { FoundLocation } from './definitions'
|
||||||
import type { DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
|
import type { ChatKind, DefinitionPeek, ExtensionToWebview, Occurrence, Reveal, WebviewToExtension } from './messages'
|
||||||
import type { TargetSpot } from './openTarget'
|
import type { TargetSpot } from './openTarget'
|
||||||
import type { TopSubject } from './pathPreview'
|
import type { TopSubject } from './pathPreview'
|
||||||
import { realpath } from 'node:fs/promises'
|
import { realpath } from 'node:fs/promises'
|
||||||
@@ -51,6 +51,9 @@ export const PREVIEW_VIEW_TYPE = 'superpowers.codeReader.preview'
|
|||||||
/** 连续输入时合并成一次重新解析。 */
|
/** 连续输入时合并成一次重新解析。 */
|
||||||
const REFRESH_DELAY_MS = 150
|
const REFRESH_DELAY_MS = 150
|
||||||
|
|
||||||
|
/** 预览里各有一个 profile 下拉的两种对话 */
|
||||||
|
const CHAT_KINDS: readonly ChatKind[] = ['ask', 'rewrite']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 一次打开预览带的话:这个标签页是从哪儿打开的(定对比什么);要滚到哪(从文本编辑器切过来时它顶部那一行,或跳到的定义);
|
* 一次打开预览带的话:这个标签页是从哪儿打开的(定对比什么);要滚到哪(从文本编辑器切过来时它顶部那一行,或跳到的定义);
|
||||||
* 要不要把光标放进顶部「问 AI」输入框(侧栏「问 AI」打开的)。
|
* 要不要把光标放进顶部「问 AI」输入框(侧栏「问 AI」打开的)。
|
||||||
@@ -290,10 +293,10 @@ class Preview {
|
|||||||
if (uris.some(uri => uri.toString() === this.subject().uri.toString()))
|
if (uris.some(uri => uri.toString() === this.subject().uri.toString()))
|
||||||
this.postQa()
|
this.postQa()
|
||||||
}),
|
}),
|
||||||
owner.qa.profiles.subscribe((choice) => {
|
...CHAT_KINDS.map(kind => owner.qa.profiles[kind].subscribe((choice) => {
|
||||||
if (this.ready)
|
if (this.ready)
|
||||||
this.post({ type: 'askProfiles', ...choice })
|
this.post({ type: 'profiles', kind, ...choice })
|
||||||
}),
|
})),
|
||||||
owner.qa.onDidAnswer((event) => {
|
owner.qa.onDidAnswer((event) => {
|
||||||
if (event.owner !== this.subject().uri.toString() || !this.ready)
|
if (event.owner !== this.subject().uri.toString() || !this.ready)
|
||||||
return
|
return
|
||||||
@@ -546,11 +549,11 @@ class Preview {
|
|||||||
this.post({ type: 'focusPathAsk' })
|
this.post({ type: 'focusPathAsk' })
|
||||||
}
|
}
|
||||||
|
|
||||||
/** profile 列表每次现读(同看板),读完页面还在才发。 */
|
/** kind 那种对话的 profile 列表每次现读(同看板),读完页面还在才发。 */
|
||||||
private async postProfiles(): Promise<void> {
|
private async postProfiles(kind: ChatKind): Promise<void> {
|
||||||
const choice = await this.owner.qa.profiles.choice()
|
const choice = await this.owner.qa.profiles[kind].choice()
|
||||||
if (this.ready)
|
if (this.ready)
|
||||||
this.post({ type: 'askProfiles', ...choice })
|
this.post({ type: 'profiles', kind, ...choice })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -601,7 +604,8 @@ class Preview {
|
|||||||
this.postHoverDelay()
|
this.postHoverDelay()
|
||||||
this.postStickyScroll()
|
this.postStickyScroll()
|
||||||
this.postQa()
|
this.postQa()
|
||||||
void this.postProfiles()
|
for (const kind of CHAT_KINDS)
|
||||||
|
void this.postProfiles(kind)
|
||||||
await this.push()
|
await this.push()
|
||||||
if (this.pendingPathFocus)
|
if (this.pendingPathFocus)
|
||||||
this.focusPathAsk()
|
this.focusPathAsk()
|
||||||
@@ -609,13 +613,13 @@ class Preview {
|
|||||||
case 'ask':
|
case 'ask':
|
||||||
await this.ask(msg.askId, msg.anchor, msg.question, msg.images)
|
await this.ask(msg.askId, msg.anchor, msg.question, msg.images)
|
||||||
return
|
return
|
||||||
case 'selectAskProfile':
|
case 'selectProfile':
|
||||||
try {
|
try {
|
||||||
await this.owner.qa.profiles.select(msg.id)
|
await this.owner.qa.profiles[msg.kind].select(msg.id)
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
void window.showErrorMessage(err instanceof Error ? err.message : String(err))
|
void window.showErrorMessage(err instanceof Error ? err.message : String(err))
|
||||||
await this.postProfiles()
|
await this.postProfiles(msg.kind)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
case 'stopAsk':
|
case 'stopAsk':
|
||||||
|
|||||||
@@ -43,11 +43,24 @@ describe('profile 列表 → 下拉的选项', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('文件问答默认模型与临时选择', () => {
|
describe('报错说清是哪一套', () => {
|
||||||
|
it('问答、重写注释各报各的名字,并指向设置里各自的默认模型', async () => {
|
||||||
|
const gone = `${DIR}/gone.json`
|
||||||
|
await expect(new AskProfiles('问答', () => gone, source()).current())
|
||||||
|
.rejects
|
||||||
|
.toThrow(`问答模型不可用:${gone}。请在设置的「问答默认模型」里重新选择,或在输入框旁临时切换。`)
|
||||||
|
await expect(new AskProfiles('重写注释', () => gone, source()).current())
|
||||||
|
.rejects
|
||||||
|
.toThrow(`重写注释模型不可用:${gone}。请在设置的「重写注释默认模型」里重新选择,或在输入框旁临时切换。`)
|
||||||
|
await expect(new AskProfiles('重写注释', () => '', source()).select('unknown')).rejects.toThrow('重写注释模型不可用:unknown。请重新选择。')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('默认模型与临时选择', () => {
|
||||||
it.each(['offical', 'official'])('临时选空默认后,%s.json 增加或删除都保持不带配置文件', async (officialName) => {
|
it.each(['offical', 'official'])('临时选空默认后,%s.json 增加或删除都保持不带配置文件', async (officialName) => {
|
||||||
let available = [profile('sonnet')]
|
let available = [profile('sonnet')]
|
||||||
let defaultPath = DEFAULT_PATH
|
let defaultPath = DEFAULT_PATH
|
||||||
const profiles = new AskProfiles(() => `${DIR}/sonnet.json`, {
|
const profiles = new AskProfiles('问答', () => `${DIR}/sonnet.json`, {
|
||||||
...source(),
|
...source(),
|
||||||
list: async () => available,
|
list: async () => available,
|
||||||
defaultPath: () => defaultPath,
|
defaultPath: () => defaultPath,
|
||||||
@@ -67,7 +80,7 @@ describe('文件问答默认模型与临时选择', () => {
|
|||||||
|
|
||||||
it('真实目录变更才清除显式空选择并恢复设置默认', async () => {
|
it('真实目录变更才清除显式空选择并恢复设置默认', async () => {
|
||||||
let directory = DIR
|
let directory = DIR
|
||||||
const profiles = new AskProfiles(() => `${DIR}/sonnet.json`, {
|
const profiles = new AskProfiles('问答', () => `${DIR}/sonnet.json`, {
|
||||||
...source([profile('sonnet')]),
|
...source([profile('sonnet')]),
|
||||||
directory: () => directory,
|
directory: () => directory,
|
||||||
})
|
})
|
||||||
@@ -81,7 +94,7 @@ describe('文件问答默认模型与临时选择', () => {
|
|||||||
it('仅官方文件删除改变自动默认路径时,保留失效的临时选择并阻止发问', async () => {
|
it('仅官方文件删除改变自动默认路径时,保留失效的临时选择并阻止发问', async () => {
|
||||||
let available = [profile('dsflash'), profile('official')]
|
let available = [profile('dsflash'), profile('official')]
|
||||||
let defaultPath = `${DIR}/official.json`
|
let defaultPath = `${DIR}/official.json`
|
||||||
const profiles = new AskProfiles(() => `${DIR}/dsflash.json`, {
|
const profiles = new AskProfiles('问答', () => `${DIR}/dsflash.json`, {
|
||||||
...source(),
|
...source(),
|
||||||
list: async () => available,
|
list: async () => available,
|
||||||
defaultPath: () => defaultPath,
|
defaultPath: () => defaultPath,
|
||||||
@@ -90,11 +103,11 @@ describe('文件问答默认模型与临时选择', () => {
|
|||||||
available = [profile('dsflash')]
|
available = [profile('dsflash')]
|
||||||
defaultPath = `${DIR}/offical.json`
|
defaultPath = `${DIR}/offical.json`
|
||||||
expect((await profiles.choice()).selected).toBe(`${DIR}/official.json`)
|
expect((await profiles.choice()).selected).toBe(`${DIR}/official.json`)
|
||||||
await expect(profiles.current()).rejects.toThrow(`文件问答模型不可用:${DIR}/official.json`)
|
await expect(profiles.current()).rejects.toThrow(`问答模型不可用:${DIR}/official.json`)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('没有官方配置文件时,可以从配置的默认模型临时切到内置默认', async () => {
|
it('没有官方配置文件时,可以从配置的默认模型临时切到内置默认', async () => {
|
||||||
const profiles = new AskProfiles(() => `${DIR}/sonnet.json`, source([profile('sonnet')]))
|
const profiles = new AskProfiles('问答', () => `${DIR}/sonnet.json`, source([profile('sonnet')]))
|
||||||
await profiles.select('')
|
await profiles.select('')
|
||||||
expect((await profiles.choice()).selected).toBe('')
|
expect((await profiles.choice()).selected).toBe('')
|
||||||
expect(await profiles.current()).toBeUndefined()
|
expect(await profiles.current()).toBeUndefined()
|
||||||
@@ -102,19 +115,19 @@ describe('文件问答默认模型与临时选择', () => {
|
|||||||
|
|
||||||
it('配置默认实际用于命令行;临时切换不改默认,重新创建服务恢复配置', async () => {
|
it('配置默认实际用于命令行;临时切换不改默认,重新创建服务恢复配置', async () => {
|
||||||
const configured = `${DIR}/dsflash.json`
|
const configured = `${DIR}/dsflash.json`
|
||||||
const profiles = new AskProfiles(() => configured, source())
|
const profiles = new AskProfiles('问答', () => configured, source())
|
||||||
expect(await profiles.current()).toEqual(profile('dsflash'))
|
expect(await profiles.current()).toEqual(profile('dsflash'))
|
||||||
expect(askArgs({ profilePath: (await profiles.current())?.path }).slice(0, 2)).toEqual(['--settings', configured])
|
expect(askArgs({ profilePath: (await profiles.current())?.path }).slice(0, 2)).toEqual(['--settings', configured])
|
||||||
await profiles.select('sonnet')
|
await profiles.select('sonnet')
|
||||||
expect((await profiles.choice()).selected).toBe('sonnet')
|
expect((await profiles.choice()).selected).toBe('sonnet')
|
||||||
expect(await profiles.current()).toEqual(profile('sonnet'))
|
expect(await profiles.current()).toEqual(profile('sonnet'))
|
||||||
expect(configured).toBe(`${DIR}/dsflash.json`)
|
expect(configured).toBe(`${DIR}/dsflash.json`)
|
||||||
expect(await new AskProfiles(() => configured, source()).current()).toEqual(profile('dsflash'))
|
expect(await new AskProfiles('问答', () => configured, source()).current()).toEqual(profile('dsflash'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('配置变更清除临时选择并通知所有预览;相同配置刷新保留临时选择', async () => {
|
it('配置变更清除临时选择并通知所有预览;相同配置刷新保留临时选择', async () => {
|
||||||
let configured = `${DIR}/dsflash.json`
|
let configured = `${DIR}/dsflash.json`
|
||||||
const profiles = new AskProfiles(() => configured, source())
|
const profiles = new AskProfiles('问答', () => configured, source())
|
||||||
const updates: string[] = []
|
const updates: string[] = []
|
||||||
profiles.subscribe(choice => updates.push(choice.selected))
|
profiles.subscribe(choice => updates.push(choice.selected))
|
||||||
await profiles.select('sonnet')
|
await profiles.select('sonnet')
|
||||||
@@ -131,29 +144,29 @@ describe('文件问答默认模型与临时选择', () => {
|
|||||||
|
|
||||||
it('配置的模型已删除时保留不可用选项并报错,不会偷偷换成官方模型', async () => {
|
it('配置的模型已删除时保留不可用选项并报错,不会偷偷换成官方模型', async () => {
|
||||||
let available = [...PROFILES]
|
let available = [...PROFILES]
|
||||||
const profiles = new AskProfiles(() => `${DIR}/sonnet.json`, { ...source(), list: async () => available })
|
const profiles = new AskProfiles('问答', () => `${DIR}/sonnet.json`, { ...source(), list: async () => available })
|
||||||
expect(await profiles.current()).toEqual(profile('sonnet'))
|
expect(await profiles.current()).toEqual(profile('sonnet'))
|
||||||
available = available.filter(item => item.name !== 'sonnet')
|
available = available.filter(item => item.name !== 'sonnet')
|
||||||
const choice = await profiles.choice()
|
const choice = await profiles.choice()
|
||||||
expect(choice.options.find(item => item.id === choice.selected)?.label).toContain('不可用')
|
expect(choice.options.find(item => item.id === choice.selected)?.label).toContain('不可用')
|
||||||
await expect(profiles.current()).rejects.toThrow('文件问答模型不可用')
|
await expect(profiles.current()).rejects.toThrow('问答模型不可用')
|
||||||
await profiles.select('dsflash')
|
await profiles.select('dsflash')
|
||||||
expect(await profiles.current()).toEqual(profile('dsflash'))
|
expect(await profiles.current()).toEqual(profile('dsflash'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('临时模型被删除和无效选择都会报错,不能静默换模型', async () => {
|
it('临时模型被删除和无效选择都会报错,不能静默换模型', async () => {
|
||||||
let available = [...PROFILES]
|
let available = [...PROFILES]
|
||||||
const profiles = new AskProfiles(() => '', { ...source(), list: async () => available })
|
const profiles = new AskProfiles('问答', () => '', { ...source(), list: async () => available })
|
||||||
await profiles.select('sonnet')
|
await profiles.select('sonnet')
|
||||||
available = available.filter(item => item.name !== 'sonnet')
|
available = available.filter(item => item.name !== 'sonnet')
|
||||||
await expect(profiles.current()).rejects.toThrow('文件问答模型不可用')
|
await expect(profiles.current()).rejects.toThrow('问答模型不可用')
|
||||||
await expect(profiles.select('unknown')).rejects.toThrow('文件问答模型不可用')
|
await expect(profiles.select('unknown')).rejects.toThrow('问答模型不可用')
|
||||||
await profiles.select('offical')
|
await profiles.select('offical')
|
||||||
expect(await profiles.current()).toEqual(profile('offical'))
|
expect(await profiles.current()).toEqual(profile('offical'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('一个预览选了,所有订阅的预览都收到新的选项和选中的那项;取消订阅后不再收到', async () => {
|
it('一个预览选了,所有订阅的预览都收到新的选项和选中的那项;取消订阅后不再收到', async () => {
|
||||||
const profiles = new AskProfiles(() => '', source())
|
const profiles = new AskProfiles('问答', () => '', source())
|
||||||
const first: ProfileChoice[] = []
|
const first: ProfileChoice[] = []
|
||||||
const second: string[] = []
|
const second: string[] = []
|
||||||
profiles.subscribe(choice => first.push(choice))
|
profiles.subscribe(choice => first.push(choice))
|
||||||
@@ -167,17 +180,17 @@ describe('文件问答默认模型与临时选择', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('下一个问题用选的那个 profile:命令行带它的 --settings;选「默认」时没有、不带 --settings', async () => {
|
it('下一个问题用选的那个 profile:命令行带它的 --settings;选「默认」时没有、不带 --settings', async () => {
|
||||||
const profiles = new AskProfiles(() => `${DIR}/sonnet.json`, source())
|
const profiles = new AskProfiles('问答', () => `${DIR}/sonnet.json`, source())
|
||||||
const chosen = await profiles.current()
|
const chosen = await profiles.current()
|
||||||
expect(chosen).toEqual(profile('sonnet'))
|
expect(chosen).toEqual(profile('sonnet'))
|
||||||
expect(askArgs({ profilePath: chosen?.path }).slice(0, 2)).toEqual(['--settings', `${DIR}/sonnet.json`])
|
expect(askArgs({ profilePath: chosen?.path }).slice(0, 2)).toEqual(['--settings', `${DIR}/sonnet.json`])
|
||||||
|
|
||||||
const none = await new AskProfiles(() => '', source([])).current()
|
const none = await new AskProfiles('问答', () => '', source([])).current()
|
||||||
expect(none).toBeUndefined()
|
expect(none).toBeUndefined()
|
||||||
expect(askArgs({ profilePath: none?.path })).not.toContain('--settings')
|
expect(askArgs({ profilePath: none?.path })).not.toContain('--settings')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('没选过:用插件的默认 profile(和以前问 AI 用的一样)', async () => {
|
it('没选过:用插件的默认 profile(和以前问 AI 用的一样)', async () => {
|
||||||
expect(await new AskProfiles(() => '', source()).current()).toEqual(profile('offical'))
|
expect(await new AskProfiles('问答', () => '', source()).current()).toEqual(profile('offical'))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -34,13 +34,18 @@ export interface ProfileSource {
|
|||||||
|
|
||||||
const PROFILE_SOURCE: ProfileSource = { list: listClaudeProfiles, defaultPath: getDefaultProfilePath, directory: getProfilesDir }
|
const PROFILE_SOURCE: ProfileSource = { list: listClaudeProfiles, defaultPath: getDefaultProfilePath, directory: getProfilesDir }
|
||||||
|
|
||||||
/** 所有预览共用临时模型选择,重新创建服务或修改默认配置后恢复设置里的默认值。 */
|
/**
|
||||||
|
* 一套 profile 选择:临时切换所有预览共用、不写进设置,重新创建服务或这一套的默认配置改了以后回到设置里的默认值。
|
||||||
|
*
|
||||||
|
* @param purpose 这一套给什么用(「问答」「重写注释」);报错时用它说是哪一套、该改设置里的哪一项
|
||||||
|
* @param configuredPath 读设置里这一套的默认 profile 路径
|
||||||
|
*/
|
||||||
export class AskProfiles {
|
export class AskProfiles {
|
||||||
private readonly listeners = new Set<(choice: ProfileChoice) => void>()
|
private readonly listeners = new Set<(choice: ProfileChoice) => void>()
|
||||||
private temporary: string | undefined
|
private temporary: string | undefined
|
||||||
private defaultKey: string | undefined
|
private defaultKey: string | undefined
|
||||||
|
|
||||||
constructor(private readonly configuredPath: () => string, private readonly source: ProfileSource = PROFILE_SOURCE) {}
|
constructor(readonly purpose: string, private readonly configuredPath: () => string, private readonly source: ProfileSource = PROFILE_SOURCE) {}
|
||||||
|
|
||||||
async choice(): Promise<ProfileChoice> {
|
async choice(): Promise<ProfileChoice> {
|
||||||
return (await this.read()).choice
|
return (await this.read()).choice
|
||||||
@@ -52,7 +57,7 @@ export class AskProfiles {
|
|||||||
this.publish(choice)
|
this.publish(choice)
|
||||||
const profile = profiles.find(profile => profile.name === choice.selected)
|
const profile = profiles.find(profile => profile.name === choice.selected)
|
||||||
if (choice.selected && !profile)
|
if (choice.selected && !profile)
|
||||||
throw new Error(`文件问答模型不可用:${choice.selected}。请在设置中重新选择默认模型,或在输入框旁临时切换。`)
|
throw new Error(`${this.purpose}模型不可用:${choice.selected}。请在设置的「${this.purpose}默认模型」里重新选择,或在输入框旁临时切换。`)
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +71,7 @@ export class AskProfiles {
|
|||||||
const { profiles, choice } = await this.read()
|
const { profiles, choice } = await this.read()
|
||||||
const profile = profiles.find(profile => profile.name === id)
|
const profile = profiles.find(profile => profile.name === id)
|
||||||
if (id ? !profile : !choice.options.some(option => option.id === ''))
|
if (id ? !profile : !choice.options.some(option => option.id === ''))
|
||||||
throw new Error(`文件问答模型不可用:${id}。请重新选择。`)
|
throw new Error(`${this.purpose}模型不可用:${id}。请重新选择。`)
|
||||||
this.temporary = profile?.path ?? ''
|
this.temporary = profile?.path ?? ''
|
||||||
await this.refresh()
|
await this.refresh()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,10 +91,10 @@ describe('问这个文件实际使用设置的默认模型', () => {
|
|||||||
await saveSettings(ctx, { defaultAskProfilePath: '/p/sonnet.json' })
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/sonnet.json' })
|
||||||
vi.mocked(listClaudeProfiles).mockResolvedValue(AVAILABLE.filter(item => item.name !== 'offical'))
|
vi.mocked(listClaudeProfiles).mockResolvedValue(AVAILABLE.filter(item => item.name !== 'offical'))
|
||||||
const { qa, runner } = service(ctx)
|
const { qa, runner } = service(ctx)
|
||||||
await qa.profiles.select('')
|
await qa.profiles.ask.select('')
|
||||||
vi.mocked(listClaudeProfiles).mockResolvedValue(AVAILABLE)
|
vi.mocked(listClaudeProfiles).mockResolvedValue(AVAILABLE)
|
||||||
const shown = vi.fn()
|
const shown = vi.fn()
|
||||||
qa.profiles.subscribe(shown)
|
qa.profiles.ask.subscribe(shown)
|
||||||
await saveSettings(ctx, { webhookPort: 9000 })
|
await saveSettings(ctx, { webhookPort: 9000 })
|
||||||
await vi.waitFor(() => expect(shown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: '' })))
|
await vi.waitFor(() => expect(shown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: '' })))
|
||||||
await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })
|
await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })
|
||||||
@@ -114,7 +114,7 @@ describe('问这个文件实际使用设置的默认模型', () => {
|
|||||||
await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })
|
await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })
|
||||||
expect(runner.mock.calls[0]?.[0].profilePath).toBe('/p/dsflash.json')
|
expect(runner.mock.calls[0]?.[0].profilePath).toBe('/p/dsflash.json')
|
||||||
qa.stop('1')
|
qa.stop('1')
|
||||||
await qa.profiles.select('sonnet')
|
await qa.profiles.ask.select('sonnet')
|
||||||
await qa.askPath(uri, 'file', { askId: '2', question: '这是什么', images: [] })
|
await qa.askPath(uri, 'file', { askId: '2', question: '这是什么', images: [] })
|
||||||
expect(runner.mock.calls[1]?.[0].profilePath).toBe('/p/sonnet.json')
|
expect(runner.mock.calls[1]?.[0].profilePath).toBe('/p/sonnet.json')
|
||||||
expect(getSettings(ctx).defaultAskProfilePath).toBe('/p/dsflash.json')
|
expect(getSettings(ctx).defaultAskProfilePath).toBe('/p/dsflash.json')
|
||||||
@@ -126,9 +126,9 @@ describe('问这个文件实际使用设置的默认模型', () => {
|
|||||||
it('保存新默认后所有现有预览收到新选择,下一个问题和界面使用同一个模型', async () => {
|
it('保存新默认后所有现有预览收到新选择,下一个问题和界面使用同一个模型', async () => {
|
||||||
const ctx = context()
|
const ctx = context()
|
||||||
const { qa, runner } = service(ctx)
|
const { qa, runner } = service(ctx)
|
||||||
await qa.profiles.select('sonnet')
|
await qa.profiles.ask.select('sonnet')
|
||||||
const shown = vi.fn()
|
const shown = vi.fn()
|
||||||
qa.profiles.subscribe(shown)
|
qa.profiles.ask.subscribe(shown)
|
||||||
await saveSettings(ctx, { defaultAskProfilePath: '/p/dsflash.json' })
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/dsflash.json' })
|
||||||
await vi.waitFor(() => expect(shown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: 'dsflash' })))
|
await vi.waitFor(() => expect(shown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: 'dsflash' })))
|
||||||
await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })
|
await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })
|
||||||
@@ -139,12 +139,84 @@ describe('问这个文件实际使用设置的默认模型', () => {
|
|||||||
const ctx = context()
|
const ctx = context()
|
||||||
await saveSettings(ctx, { defaultAskProfilePath: '/p/deleted.json' })
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/deleted.json' })
|
||||||
const { qa, runner } = service(ctx)
|
const { qa, runner } = service(ctx)
|
||||||
expect(await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })).toContain('文件问答模型不可用')
|
expect(await qa.askPath(uri, 'file', { askId: '1', question: '这是什么', images: [] })).toMatch(/^问答模型不可用:\/p\/deleted\.json。/)
|
||||||
expect(await qa.ask(uri, { askId: '2', anchor: 'a', question: '问题', prompt: '问题', followUp: '继续', images: [] })).toContain('文件问答模型不可用')
|
expect(await qa.ask(uri, { askId: '2', anchor: 'a', question: '问题', prompt: '问题', followUp: '继续', images: [] })).toMatch(/^问答模型不可用:\/p\/deleted\.json。/)
|
||||||
expect(runner).not.toHaveBeenCalled()
|
expect(runner).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('问 AI 和重写注释各用各的 profile', () => {
|
||||||
|
const SKILL = '/ecc:writing-code-comments a.ts:1-3'
|
||||||
|
const asking = (askId: string) => ({ askId, anchor: 'function f', question: '为什么', prompt: '完整的', followUp: '短的', images: [] })
|
||||||
|
const rewriting = (askId: string): RewriteRequest => ({ askId, anchor: 'function f', question: SKILL, prompt: SKILL, images: [], writeBack: async () => undefined })
|
||||||
|
|
||||||
|
it('各按设置里自己的默认模型起步:块下面问、顶部问、了解一块代码用问答的,重写注释用重写的', async () => {
|
||||||
|
const ctx = context()
|
||||||
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/sonnet.json', defaultRewriteProfilePath: '/p/dsflash.json' })
|
||||||
|
const { qa, runner, rewriter } = service(ctx)
|
||||||
|
rewriter.mockResolvedValue({ body: '新的。', sessionId: 'r1' })
|
||||||
|
await qa.ask(uri, asking('1'))
|
||||||
|
await qa.askPath(uri, 'file', { askId: '2', question: '这是什么', images: [] })
|
||||||
|
qa.stop('2')
|
||||||
|
await qa.askTop(uri, { askId: '3', question: '用户登录', images: [] }, { prompt: '找', followUp: '接着找' }, async answer => ({ text: answer.text }))
|
||||||
|
await qa.rewrite(uri, rewriting('4'))
|
||||||
|
expect(runner.mock.calls.map(([run]) => run.profilePath)).toEqual(['/p/sonnet.json', '/p/sonnet.json', '/p/sonnet.json'])
|
||||||
|
expect(rewriter.mock.calls[0]?.[0].profilePath).toBe('/p/dsflash.json')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('都没配时两套都用默认的那个', async () => {
|
||||||
|
const { qa, runner, rewriter } = service(context())
|
||||||
|
rewriter.mockResolvedValue({ body: '新的。', sessionId: 'r1' })
|
||||||
|
await qa.ask(uri, asking('1'))
|
||||||
|
await qa.rewrite(uri, rewriting('2'))
|
||||||
|
expect(runner.mock.calls[0]?.[0].profilePath).toBe('/p/offical.json')
|
||||||
|
expect(rewriter.mock.calls[0]?.[0].profilePath).toBe('/p/offical.json')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('临时切换只改自己那一套、只通知自己那一套的下拉,不写进设置', async () => {
|
||||||
|
const ctx = context()
|
||||||
|
const { qa, runner, rewriter } = service(ctx)
|
||||||
|
rewriter.mockResolvedValue({ body: '新的。', sessionId: 'r1' })
|
||||||
|
const askShown = vi.fn()
|
||||||
|
const rewriteShown = vi.fn()
|
||||||
|
qa.profiles.ask.subscribe(askShown)
|
||||||
|
qa.profiles.rewrite.subscribe(rewriteShown)
|
||||||
|
await qa.profiles.rewrite.select('dsflash')
|
||||||
|
expect(rewriteShown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: 'dsflash' }))
|
||||||
|
expect(askShown).not.toHaveBeenCalled()
|
||||||
|
await qa.profiles.ask.select('sonnet')
|
||||||
|
expect(askShown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: 'sonnet' }))
|
||||||
|
expect(rewriteShown).toHaveBeenCalledTimes(1)
|
||||||
|
await qa.ask(uri, asking('1'))
|
||||||
|
await qa.rewrite(uri, rewriting('2'))
|
||||||
|
expect(runner.mock.calls[0]?.[0].profilePath).toBe('/p/sonnet.json')
|
||||||
|
expect(rewriter.mock.calls[0]?.[0].profilePath).toBe('/p/dsflash.json')
|
||||||
|
expect(getSettings(ctx)).toMatchObject({ defaultAskProfilePath: '', defaultRewriteProfilePath: '' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('改了重写注释的默认模型只清掉重写那一套的临时选择,问答那一套的留着', async () => {
|
||||||
|
const ctx = context()
|
||||||
|
const { qa } = service(ctx)
|
||||||
|
await qa.profiles.ask.select('sonnet')
|
||||||
|
await qa.profiles.rewrite.select('sonnet')
|
||||||
|
const rewriteShown = vi.fn()
|
||||||
|
qa.profiles.rewrite.subscribe(rewriteShown)
|
||||||
|
await saveSettings(ctx, { defaultRewriteProfilePath: '/p/dsflash.json' })
|
||||||
|
await vi.waitFor(() => expect(rewriteShown).toHaveBeenLastCalledWith(expect.objectContaining({ selected: 'dsflash' })))
|
||||||
|
expect((await qa.profiles.ask.choice()).selected).toBe('sonnet')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('重写注释用的模型删掉了:报重写注释的错、不启动模型;问答照常', async () => {
|
||||||
|
const ctx = context()
|
||||||
|
await saveSettings(ctx, { defaultRewriteProfilePath: '/p/deleted.json' })
|
||||||
|
const { qa, runner, rewriter } = service(ctx)
|
||||||
|
expect(await qa.rewrite(uri, rewriting('1'))).toMatch(/^重写注释模型不可用:\/p\/deleted\.json。/)
|
||||||
|
expect(rewriter).not.toHaveBeenCalled()
|
||||||
|
expect(await qa.ask(uri, asking('2'))).toBeUndefined()
|
||||||
|
expect(runner).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('重写注释的记录', () => {
|
describe('重写注释的记录', () => {
|
||||||
const SKILL = '/ecc:writing-code-comments a.ts:1-3'
|
const SKILL = '/ecc:writing-code-comments a.ts:1-3'
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Event, ExtensionContext, Memento } from 'vscode'
|
import type { Event, ExtensionContext, Memento } from 'vscode'
|
||||||
import type { ClaudeProfile } from '../../cc/profiles'
|
import type { ClaudeProfile } from '../../cc/profiles'
|
||||||
import type { ClaudeImage } from '../../cc/spawnClaude'
|
import type { ClaudeImage } from '../../cc/spawnClaude'
|
||||||
import type { QaEntryView, QaMessage } from '../editor/messages'
|
import type { ChatKind, QaEntryView, QaMessage } from '../editor/messages'
|
||||||
import type { PathSubject, TopSubject } from '../editor/pathPreview'
|
import type { PathSubject, TopSubject } from '../editor/pathPreview'
|
||||||
import type { PathLocation } from '../folderStore'
|
import type { PathLocation } from '../folderStore'
|
||||||
import type { AskEnd, RunningAsk } from './asks'
|
import type { AskEnd, RunningAsk } from './asks'
|
||||||
@@ -121,8 +121,8 @@ export class CodeQa implements Disposable {
|
|||||||
/** 顶部问答的读写排成一队,两次同时答完不会互相盖掉 */
|
/** 顶部问答的读写排成一队,两次同时答完不会互相盖掉 */
|
||||||
private pathQueue: Promise<void> = Promise.resolve()
|
private pathQueue: Promise<void> = Promise.resolve()
|
||||||
private readonly settingsSubscription: Disposable
|
private readonly settingsSubscription: Disposable
|
||||||
/** 所有预览共用当前模型,临时切换不会写入默认设置。 */
|
/** 问 AI(块下面和顶部)、重写注释各用各的一套 profile,各自的临时切换所有预览共用、不写进设置 */
|
||||||
readonly profiles: AskProfiles
|
readonly profiles: Readonly<Record<ChatKind, AskProfiles>>
|
||||||
/** 块下面的问答、重写记录或顶部的问答变了的那些文件 / 文件夹 */
|
/** 块下面的问答、重写记录或顶部的问答变了的那些文件 / 文件夹 */
|
||||||
readonly onDidChange: Event<Uri[]> = (listener, thisArgs, disposables) => {
|
readonly onDidChange: Event<Uri[]> = (listener, thisArgs, disposables) => {
|
||||||
const subscription = Disposable.from(
|
const subscription = Disposable.from(
|
||||||
@@ -143,11 +143,16 @@ export class CodeQa implements Disposable {
|
|||||||
runner: AskRunner = askClaude,
|
runner: AskRunner = askClaude,
|
||||||
rewriter: (ask: CommentAsk) => Promise<CommentAnswer> = askCommentBody,
|
rewriter: (ask: CommentAsk) => Promise<CommentAnswer> = askCommentBody,
|
||||||
) {
|
) {
|
||||||
this.profiles = new AskProfiles(() => getSettings(context).defaultAskProfilePath)
|
this.profiles = {
|
||||||
|
ask: new AskProfiles('问答', () => getSettings(context).defaultAskProfilePath),
|
||||||
|
rewrite: new AskProfiles('重写注释', () => getSettings(context).defaultRewriteProfilePath),
|
||||||
|
}
|
||||||
this.settingsSubscription = onDidChangeSettings(context, () => {
|
this.settingsSubscription = onDidChangeSettings(context, () => {
|
||||||
void this.profiles.refresh().catch((err) => {
|
for (const profiles of Object.values(this.profiles)) {
|
||||||
void window.showErrorMessage(`文件问答模型刷新失败:${err instanceof Error ? err.message : String(err)}`)
|
void profiles.refresh().catch((err) => {
|
||||||
|
void window.showErrorMessage(`${profiles.purpose}模型刷新失败:${err instanceof Error ? err.message : String(err)}`)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
this.asks = new QaAsks(runner)
|
this.asks = new QaAsks(runner)
|
||||||
this.rewrites = new QaAsks(rewriter, REWRITE_POLICY)
|
this.rewrites = new QaAsks(rewriter, REWRITE_POLICY)
|
||||||
@@ -188,11 +193,11 @@ export class CodeQa implements Disposable {
|
|||||||
/**
|
/**
|
||||||
* 开始问;问不了时返回原因(给人看的话)。在工作区文件夹里跑 claude,它读得到整个项目。
|
* 开始问;问不了时返回原因(给人看的话)。在工作区文件夹里跑 claude,它读得到整个项目。
|
||||||
* 这块问过的就接着最近一次的会话问(收起再展开、重启 VS Code 都一样),没问过就带上代码上下文开新会话。
|
* 这块问过的就接着最近一次的会话问(收起再展开、重启 VS Code 都一样),没问过就带上代码上下文开新会话。
|
||||||
* 用眼下选的 profile;和那次会话开头用的不一样也照样接着问(`--resume` 和 `--settings` 能一起用)。
|
* 用问 AI 眼下选的 profile;和那次会话开头用的不一样也照样接着问(`--resume` 和 `--settings` 能一起用)。
|
||||||
* 带的图片随问题发,答成了才写进 `.spx/code-qa/`、记在这条上;没答成不留文件。
|
* 带的图片随问题发,答成了才写进 `.spx/code-qa/`、记在这条上;没答成不留文件。
|
||||||
*/
|
*/
|
||||||
async ask(uri: Uri, request: AskRequest): Promise<string | undefined> {
|
async ask(uri: Uri, request: AskRequest): Promise<string | undefined> {
|
||||||
const ready = await this.prepare(uri, request.images, '问答')
|
const ready = await this.prepare(uri, request.images, 'ask')
|
||||||
if (typeof ready === 'string')
|
if (typeof ready === 'string')
|
||||||
return ready
|
return ready
|
||||||
const { at, profile } = ready
|
const { at, profile } = ready
|
||||||
@@ -230,12 +235,12 @@ export class CodeQa implements Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重写一段注释;开不了时返回原因(给人看的话)。在工作区文件夹里跑 claude(见 askCommentBody),用眼下选的 profile。
|
* 重写一段注释;开不了时返回原因(给人看的话)。在工作区文件夹里跑 claude(见 [askCommentBody]),用重写注释眼下选的 profile。
|
||||||
* 带 followUp 时接着这块最近一次重写的会话说(和问答的会话分开),没重写过就开新会话;不带时(点按钮)总是开新会话。
|
* 带 followUp 时接着这块最近一次重写的会话说(和问答的会话分开),没重写过就开新会话;不带时(点按钮)总是开新会话。
|
||||||
* 模型答完先写回文件,再把这一轮记下:用户的话、交回的正文、没写进文件的原因、会话 id、profile、图片。Claude Code 报错、被叫停的不记,文件不动。
|
* 模型答完先写回文件,再把这一轮记下:用户的话、交回的正文、没写进文件的原因、会话 id、profile、图片。Claude Code 报错、被叫停的不记,文件不动。
|
||||||
*/
|
*/
|
||||||
async rewrite(uri: Uri, request: RewriteRequest): Promise<string | undefined> {
|
async rewrite(uri: Uri, request: RewriteRequest): Promise<string | undefined> {
|
||||||
const ready = await this.prepare(uri, request.images, '重写记录')
|
const ready = await this.prepare(uri, request.images, 'rewrite')
|
||||||
if (typeof ready === 'string')
|
if (typeof ready === 'string')
|
||||||
return ready
|
return ready
|
||||||
const { at, profile } = ready
|
const { at, profile } = ready
|
||||||
@@ -295,15 +300,15 @@ export class CodeQa implements Disposable {
|
|||||||
await this.rewriteStore.update(at.folder, map => withoutEntry(map, at.key, anchor, entryId))
|
await this.rewriteStore.update(at.folder, map => withoutEntry(map, at.key, anchor, entryId))
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 块下面开一轮前都要的:文件在工作区文件夹里(记录才有地方存)、图片格式认得、眼下选的 profile 还在;不行时返回原因。 */
|
/** 块下面开一轮 kind 那种对话前都要的:文件在工作区文件夹里(记录才有地方存)、图片格式认得、这种对话眼下选的 profile 还在;不行时返回原因。 */
|
||||||
private async prepare(uri: Uri, images: readonly ClaudeImage[], record: string): Promise<{ at: PathLocation, profile?: ClaudeProfile } | string> {
|
private async prepare(uri: Uri, images: readonly ClaudeImage[], kind: ChatKind): Promise<{ at: PathLocation, profile?: ClaudeProfile } | string> {
|
||||||
const at = locate(uri)
|
const at = locate(uri)
|
||||||
if (!at)
|
if (!at)
|
||||||
return `这个文件不在工作区文件夹里,${record}没地方存。`
|
return `这个文件不在工作区文件夹里,${kind === 'ask' ? '问答' : '重写记录'}没地方存。`
|
||||||
if (!images.every(image => isQaImageType(image.mediaType)))
|
if (!images.every(image => isQaImageType(image.mediaType)))
|
||||||
return '只能带 PNG、JPEG、GIF、WebP 图片。'
|
return '只能带 PNG、JPEG、GIF、WebP 图片。'
|
||||||
try {
|
try {
|
||||||
const profile = await this.profiles.current()
|
const profile = await this.profiles[kind].current()
|
||||||
return { at, ...(profile && { profile }) }
|
return { at, ...(profile && { profile }) }
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@@ -322,7 +327,7 @@ export class CodeQa implements Disposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 在 subject 这一页的顶部问一轮(文件、文件夹、「了解一块代码」的功能,记在哪见 topQaLocation);问不了、进行中再问时返回给人看的原因。
|
* 在 subject 这一页的顶部问一轮(文件、文件夹、「了解一块代码」的功能,记在哪见 [topQaLocation]),用问 AI 眼下选的 profile;问不了、进行中再问时返回给人看的原因。
|
||||||
* 问过的接着最近一次的会话说 run.followUp,没问过、会话找不到了发 run.prompt;要结构化结果时带上 run.schema,别的目录要读时带上 run.addDirs。
|
* 问过的接着最近一次的会话说 run.followUp,没问过、会话找不到了发 run.prompt;要结构化结果时带上 run.schema,别的目录要读时带上 run.addDirs。
|
||||||
* 答完交给 settle:它给出这一轮记下的回答,或说交回的不合格(这一轮算没答成)。答成了记在 workspaceState 里,带的图片写进 `.spx/code-qa/`;没答成不记。
|
* 答完交给 settle:它给出这一轮记下的回答,或说交回的不合格(这一轮算没答成)。答成了记在 workspaceState 里,带的图片写进 `.spx/code-qa/`;没答成不记。
|
||||||
*/
|
*/
|
||||||
@@ -335,7 +340,7 @@ export class CodeQa implements Disposable {
|
|||||||
return '只能带 PNG、JPEG、GIF、WebP 图片。'
|
return '只能带 PNG、JPEG、GIF、WebP 图片。'
|
||||||
let profile: ClaudeProfile | undefined
|
let profile: ClaudeProfile | undefined
|
||||||
try {
|
try {
|
||||||
profile = await this.profiles.current()
|
profile = await this.profiles.ask.current()
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
return err instanceof Error ? err.message : String(err)
|
return err instanceof Error ? err.message : String(err)
|
||||||
|
|||||||
@@ -1076,6 +1076,7 @@ export class KanbanWebviewPanel {
|
|||||||
defaultTestProfilePath: s.defaultTestProfilePath,
|
defaultTestProfilePath: s.defaultTestProfilePath,
|
||||||
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
|
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
|
||||||
defaultAskProfilePath: s.defaultAskProfilePath,
|
defaultAskProfilePath: s.defaultAskProfilePath,
|
||||||
|
defaultRewriteProfilePath: s.defaultRewriteProfilePath,
|
||||||
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
|
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
|
||||||
commitProfilePath: await getEffectiveCommitProfilePath(this.context),
|
commitProfilePath: await getEffectiveCommitProfilePath(this.context),
|
||||||
prDiffSummaryProfilePath: await getEffectivePrDiffSummaryProfilePath(this.context),
|
prDiffSummaryProfilePath: await getEffectivePrDiffSummaryProfilePath(this.context),
|
||||||
@@ -1170,6 +1171,7 @@ export class KanbanWebviewPanel {
|
|||||||
defaultTestProfilePath: s.defaultTestProfilePath,
|
defaultTestProfilePath: s.defaultTestProfilePath,
|
||||||
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
|
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
|
||||||
defaultAskProfilePath: s.defaultAskProfilePath,
|
defaultAskProfilePath: s.defaultAskProfilePath,
|
||||||
|
defaultRewriteProfilePath: s.defaultRewriteProfilePath,
|
||||||
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
|
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
|
||||||
commitProfilePath: await getEffectiveCommitProfilePath(this.context),
|
commitProfilePath: await getEffectiveCommitProfilePath(this.context),
|
||||||
prDiffSummaryProfilePath: await getEffectivePrDiffSummaryProfilePath(this.context),
|
prDiffSummaryProfilePath: await getEffectivePrDiffSummaryProfilePath(this.context),
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
|||||||
defaultTestProfilePath: string
|
defaultTestProfilePath: string
|
||||||
defaultPrototypeProfilePath: string
|
defaultPrototypeProfilePath: string
|
||||||
defaultAskProfilePath: string
|
defaultAskProfilePath: string
|
||||||
|
defaultRewriteProfilePath: string
|
||||||
conflictResolutionProfilePath: string
|
conflictResolutionProfilePath: string
|
||||||
commitProfilePath: string
|
commitProfilePath: string
|
||||||
prDiffSummaryProfilePath: string
|
prDiffSummaryProfilePath: string
|
||||||
@@ -87,6 +88,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
|||||||
const trimmedDefaultTestProfile = payload.defaultTestProfilePath.trim()
|
const trimmedDefaultTestProfile = payload.defaultTestProfilePath.trim()
|
||||||
const trimmedDefaultPrototypeProfile = payload.defaultPrototypeProfilePath.trim()
|
const trimmedDefaultPrototypeProfile = payload.defaultPrototypeProfilePath.trim()
|
||||||
const trimmedDefaultAskProfile = payload.defaultAskProfilePath.trim()
|
const trimmedDefaultAskProfile = payload.defaultAskProfilePath.trim()
|
||||||
|
const trimmedDefaultRewriteProfile = payload.defaultRewriteProfilePath.trim()
|
||||||
// conflict-resolution profile: '' is meaningful (= DEFAULT_PROFILE_PATH).
|
// conflict-resolution profile: '' is meaningful (= DEFAULT_PROFILE_PATH).
|
||||||
const trimmedConflictProfile = payload.conflictResolutionProfilePath.trim()
|
const trimmedConflictProfile = payload.conflictResolutionProfilePath.trim()
|
||||||
// 提交代码 profile:'' 有意义(= 未配置 → 提交严格失败),只 trim。
|
// 提交代码 profile:'' 有意义(= 未配置 → 提交严格失败),只 trim。
|
||||||
@@ -137,6 +139,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
|||||||
defaultTestProfilePath: trimmedDefaultTestProfile,
|
defaultTestProfilePath: trimmedDefaultTestProfile,
|
||||||
defaultPrototypeProfilePath: trimmedDefaultPrototypeProfile,
|
defaultPrototypeProfilePath: trimmedDefaultPrototypeProfile,
|
||||||
defaultAskProfilePath: trimmedDefaultAskProfile,
|
defaultAskProfilePath: trimmedDefaultAskProfile,
|
||||||
|
defaultRewriteProfilePath: trimmedDefaultRewriteProfile,
|
||||||
conflictResolutionProfilePath: trimmedConflictProfile,
|
conflictResolutionProfilePath: trimmedConflictProfile,
|
||||||
commitProfilePath: trimmedCommitProfile,
|
commitProfilePath: trimmedCommitProfile,
|
||||||
prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile,
|
prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile,
|
||||||
@@ -172,6 +175,7 @@ export async function handleSettingsSave(panel: KanbanWebviewPanel, payload: {
|
|||||||
defaultTestProfilePath: trimmedDefaultTestProfile,
|
defaultTestProfilePath: trimmedDefaultTestProfile,
|
||||||
defaultPrototypeProfilePath: trimmedDefaultPrototypeProfile,
|
defaultPrototypeProfilePath: trimmedDefaultPrototypeProfile,
|
||||||
defaultAskProfilePath: trimmedDefaultAskProfile,
|
defaultAskProfilePath: trimmedDefaultAskProfile,
|
||||||
|
defaultRewriteProfilePath: trimmedDefaultRewriteProfile,
|
||||||
conflictResolutionProfilePath: trimmedConflictProfile,
|
conflictResolutionProfilePath: trimmedConflictProfile,
|
||||||
commitProfilePath: trimmedCommitProfile,
|
commitProfilePath: trimmedCommitProfile,
|
||||||
prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile,
|
prDiffSummaryProfilePath: trimmedPrDiffSummaryProfile,
|
||||||
@@ -282,6 +286,7 @@ export async function handleEditSettingsRequest(panel: KanbanWebviewPanel): Prom
|
|||||||
defaultTestProfilePath: s.defaultTestProfilePath,
|
defaultTestProfilePath: s.defaultTestProfilePath,
|
||||||
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
|
defaultPrototypeProfilePath: s.defaultPrototypeProfilePath,
|
||||||
defaultAskProfilePath: s.defaultAskProfilePath,
|
defaultAskProfilePath: s.defaultAskProfilePath,
|
||||||
|
defaultRewriteProfilePath: s.defaultRewriteProfilePath,
|
||||||
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
|
conflictResolutionProfilePath: s.conflictResolutionProfilePath,
|
||||||
commitProfilePath,
|
commitProfilePath,
|
||||||
prDiffSummaryProfilePath,
|
prDiffSummaryProfilePath,
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export type ExtensionToWebview
|
|||||||
defaultTestProfilePath: string
|
defaultTestProfilePath: string
|
||||||
defaultPrototypeProfilePath: string
|
defaultPrototypeProfilePath: string
|
||||||
defaultAskProfilePath: string
|
defaultAskProfilePath: string
|
||||||
|
defaultRewriteProfilePath: string
|
||||||
conflictResolutionProfilePath: string
|
conflictResolutionProfilePath: string
|
||||||
commitProfilePath: string
|
commitProfilePath: string
|
||||||
prDiffSummaryProfilePath: string
|
prDiffSummaryProfilePath: string
|
||||||
@@ -182,6 +183,7 @@ export type WebviewToExtension
|
|||||||
defaultTestProfilePath: string
|
defaultTestProfilePath: string
|
||||||
defaultPrototypeProfilePath: string
|
defaultPrototypeProfilePath: string
|
||||||
defaultAskProfilePath: string
|
defaultAskProfilePath: string
|
||||||
|
defaultRewriteProfilePath: string
|
||||||
conflictResolutionProfilePath: string
|
conflictResolutionProfilePath: string
|
||||||
commitProfilePath: string
|
commitProfilePath: string
|
||||||
prDiffSummaryProfilePath: string
|
prDiffSummaryProfilePath: string
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ describe('saveSettings 落盘', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('文件问答默认模型独立落盘,重启读取且不受旧的上次选择影响', async () => {
|
it('问答默认模型独立落盘,重启读取且不受旧的上次选择影响', async () => {
|
||||||
const dir = tempDir()
|
const dir = tempDir()
|
||||||
const ctx = fakeCtx(dir, { 'superpowers.codeReader.askProfile': 'sonnet' })
|
const ctx = fakeCtx(dir, { 'superpowers.codeReader.askProfile': 'sonnet' })
|
||||||
expect(getSettings(ctx).defaultAskProfilePath).toBe('')
|
expect(getSettings(ctx).defaultAskProfilePath).toBe('')
|
||||||
@@ -71,6 +71,16 @@ describe('saveSettings 落盘', () => {
|
|||||||
expect(getSettings(fakeCtx(dir)).defaultAskProfilePath).toBe('')
|
expect(getSettings(fakeCtx(dir)).defaultAskProfilePath).toBe('')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('问答、重写注释的默认模型各存各的:没配时都是空(用默认),改一个不动另一个', async () => {
|
||||||
|
const dir = tempDir()
|
||||||
|
const ctx = fakeCtx(dir)
|
||||||
|
expect(getSettings(ctx).defaultRewriteProfilePath).toBe('')
|
||||||
|
await saveSettings(ctx, { defaultAskProfilePath: '/p/sonnet.json', defaultRewriteProfilePath: ' /p/dsflash.json ' })
|
||||||
|
expect(getSettings(fakeCtx(dir))).toMatchObject({ defaultAskProfilePath: '/p/sonnet.json', defaultRewriteProfilePath: '/p/dsflash.json' })
|
||||||
|
await saveSettings(ctx, { defaultRewriteProfilePath: '' })
|
||||||
|
expect(getSettings(fakeCtx(dir))).toMatchObject({ defaultAskProfilePath: '/p/sonnet.json', defaultRewriteProfilePath: '' })
|
||||||
|
})
|
||||||
|
|
||||||
it('默认配置保存后通知订阅者,取消订阅后不再通知', async () => {
|
it('默认配置保存后通知订阅者,取消订阅后不再通知', async () => {
|
||||||
const ctx = fakeCtx(tempDir())
|
const ctx = fakeCtx(tempDir())
|
||||||
const { onDidChangeSettings } = await import('./store.js')
|
const { onDidChangeSettings } = await import('./store.js')
|
||||||
|
|||||||
@@ -211,7 +211,10 @@ export interface Settings {
|
|||||||
defaultTestProfilePath: string
|
defaultTestProfilePath: string
|
||||||
/** 原型会话的默认 Claude profile;工单级「原型配置文件」为空时用它。 */
|
/** 原型会话的默认 Claude profile;工单级「原型配置文件」为空时用它。 */
|
||||||
defaultPrototypeProfilePath: string
|
defaultPrototypeProfilePath: string
|
||||||
|
/** 代码阅读里问 AI(块下面、预览顶部、「了解一块代码」)的默认 Claude profile;空串时用插件的默认 profile,它也没有就不带 `--settings`。 */
|
||||||
defaultAskProfilePath: string
|
defaultAskProfilePath: string
|
||||||
|
/** 代码阅读里重写注释的默认 Claude profile;空串时用插件的默认 profile,它也没有就不带 `--settings`。 */
|
||||||
|
defaultRewriteProfilePath: string
|
||||||
/**
|
/**
|
||||||
* Claude settings profile path for the conflict-resolution cc session
|
* Claude settings profile path for the conflict-resolution cc session
|
||||||
* (drag to 完成 when PR merge conflicts). Empty = DEFAULT_PROFILE_PATH.
|
* (drag to 完成 when PR merge conflicts). Empty = DEFAULT_PROFILE_PATH.
|
||||||
@@ -317,6 +320,7 @@ function defaults(ctx: ExtensionContext): Settings {
|
|||||||
defaultTestProfilePath: '',
|
defaultTestProfilePath: '',
|
||||||
defaultPrototypeProfilePath: '',
|
defaultPrototypeProfilePath: '',
|
||||||
defaultAskProfilePath: '',
|
defaultAskProfilePath: '',
|
||||||
|
defaultRewriteProfilePath: '',
|
||||||
conflictResolutionProfilePath: '',
|
conflictResolutionProfilePath: '',
|
||||||
commitProfilePath: '',
|
commitProfilePath: '',
|
||||||
prDiffSummaryProfilePath: '',
|
prDiffSummaryProfilePath: '',
|
||||||
@@ -435,6 +439,9 @@ export function getSettings(ctx: ExtensionContext): Settings {
|
|||||||
const defaultAskProfilePath = typeof stored.defaultAskProfilePath === 'string'
|
const defaultAskProfilePath = typeof stored.defaultAskProfilePath === 'string'
|
||||||
? stored.defaultAskProfilePath.trim()
|
? stored.defaultAskProfilePath.trim()
|
||||||
: base.defaultAskProfilePath
|
: base.defaultAskProfilePath
|
||||||
|
const defaultRewriteProfilePath = typeof stored.defaultRewriteProfilePath === 'string'
|
||||||
|
? stored.defaultRewriteProfilePath.trim()
|
||||||
|
: base.defaultRewriteProfilePath
|
||||||
// conflict-resolution profile: '' is meaningful (= DEFAULT_PROFILE_PATH at
|
// conflict-resolution profile: '' is meaningful (= DEFAULT_PROFILE_PATH at
|
||||||
// launch time), so don't coerce. Non-string legacy values collapse to ''.
|
// launch time), so don't coerce. Non-string legacy values collapse to ''.
|
||||||
const conflictResolutionProfilePath = typeof stored.conflictResolutionProfilePath === 'string'
|
const conflictResolutionProfilePath = typeof stored.conflictResolutionProfilePath === 'string'
|
||||||
@@ -496,6 +503,7 @@ export function getSettings(ctx: ExtensionContext): Settings {
|
|||||||
defaultTestProfilePath,
|
defaultTestProfilePath,
|
||||||
defaultPrototypeProfilePath,
|
defaultPrototypeProfilePath,
|
||||||
defaultAskProfilePath,
|
defaultAskProfilePath,
|
||||||
|
defaultRewriteProfilePath,
|
||||||
conflictResolutionProfilePath,
|
conflictResolutionProfilePath,
|
||||||
commitProfilePath,
|
commitProfilePath,
|
||||||
prDiffSummaryProfilePath,
|
prDiffSummaryProfilePath,
|
||||||
|
|||||||
@@ -606,6 +606,7 @@ export function App() {
|
|||||||
initialDefaultTestProfilePath={settings?.defaultTestProfilePath ?? ''}
|
initialDefaultTestProfilePath={settings?.defaultTestProfilePath ?? ''}
|
||||||
initialDefaultPrototypeProfilePath={settings?.defaultPrototypeProfilePath ?? ''}
|
initialDefaultPrototypeProfilePath={settings?.defaultPrototypeProfilePath ?? ''}
|
||||||
initialDefaultAskProfilePath={settings?.defaultAskProfilePath ?? ''}
|
initialDefaultAskProfilePath={settings?.defaultAskProfilePath ?? ''}
|
||||||
|
initialDefaultRewriteProfilePath={settings?.defaultRewriteProfilePath ?? ''}
|
||||||
initialConflictResolutionProfilePath={settings?.conflictResolutionProfilePath ?? ''}
|
initialConflictResolutionProfilePath={settings?.conflictResolutionProfilePath ?? ''}
|
||||||
initialCommitProfilePath={settings?.commitProfilePath ?? ''}
|
initialCommitProfilePath={settings?.commitProfilePath ?? ''}
|
||||||
initialPrDiffSummaryProfilePath={settings?.prDiffSummaryProfilePath ?? ''}
|
initialPrDiffSummaryProfilePath={settings?.prDiffSummaryProfilePath ?? ''}
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ export function App(): ReactElement | null {
|
|||||||
const qa = useQa()
|
const qa = useQa()
|
||||||
/** 侧栏「问 AI」要聚焦顶部输入框时加一 */
|
/** 侧栏「问 AI」要聚焦顶部输入框时加一 */
|
||||||
const [pathFocus, setPathFocus] = useState(0)
|
const [pathFocus, setPathFocus] = useState(0)
|
||||||
/** 按注释规范重写:和问 AI 共用 profile 下拉 */
|
/** 按注释规范重写 */
|
||||||
const rewrite = useRewrite(qa.state.profiles, qa.onSelectProfile)
|
const rewrite = useRewrite()
|
||||||
const [doc, setDoc] = useState<DocumentMessage | null>(null)
|
const [doc, setDoc] = useState<DocumentMessage | null>(null)
|
||||||
const [view, setView] = useState<BlockModes>(savedBlockModes)
|
const [view, setView] = useState<BlockModes>(savedBlockModes)
|
||||||
const [focusId, setFocusId] = useState<string | null>(null)
|
const [focusId, setFocusId] = useState<string | null>(null)
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const CARD: BlockView = {
|
|||||||
|
|
||||||
/** 重写注释的状态和动作:动作什么都不做 */
|
/** 重写注释的状态和动作:动作什么都不做 */
|
||||||
function rewriteTree(state: ChatState): RewriteTree {
|
function rewriteTree(state: ChatState): RewriteTree {
|
||||||
return { state, profiles: { options: [], selected: '' }, onRewrite: () => {}, onAsk: () => {}, onStop: () => {}, onDelete: () => {}, onOpen: () => {}, onClose: () => {}, onSelectProfile: () => {} }
|
return { state, onRewrite: () => {}, onAsk: () => {}, onStop: () => {}, onDelete: () => {}, onOpen: () => {}, onClose: () => {}, onSelectProfile: () => {} }
|
||||||
}
|
}
|
||||||
|
|
||||||
function state(collapsed: ReadonlySet<string>, rewrite: ChatState = { ...EMPTY_CHAT, available: true }): BlockTreeState {
|
function state(collapsed: ReadonlySet<string>, rewrite: ChatState = { ...EMPTY_CHAT, available: true }): BlockTreeState {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { DiffTree, FoldedRuns, PlacedRemoved } from './diffView'
|
|||||||
import type { GridSlots } from './PropertyGrid'
|
import type { GridSlots } from './PropertyGrid'
|
||||||
import type { ChatState, QaState } from './qa'
|
import type { ChatState, QaState } from './qa'
|
||||||
import type { PendingImage } from './qaImages'
|
import type { PendingImage } from './qaImages'
|
||||||
import type { BlockMarks, BlockView, ProfileChoice } from './types'
|
import type { BlockMarks, BlockView } from './types'
|
||||||
import { ChevronDown, ChevronRight, ChevronsUpDown, EyeOff } from 'lucide-react'
|
import { ChevronDown, ChevronRight, ChevronsUpDown, EyeOff } from 'lucide-react'
|
||||||
import { Fragment, useState } from 'react'
|
import { Fragment, useState } from 'react'
|
||||||
import { AskThread, REWRITE_THREAD } from './AskThread'
|
import { AskThread, REWRITE_THREAD } from './AskThread'
|
||||||
@@ -67,10 +67,9 @@ export interface BlockTreeState {
|
|||||||
onOpenRun: (id: string) => void
|
onOpenRun: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 重写注释的状态和动作,块按自己的记号(anchor)取;profile 下拉和问 AI 共用 */
|
/** 重写注释的状态和动作,块按自己的记号(anchor)取;profile 下拉是自己的,和问 AI 的互不影响 */
|
||||||
export interface RewriteTree {
|
export interface RewriteTree {
|
||||||
state: ChatState
|
state: ChatState
|
||||||
profiles: ProfileChoice
|
|
||||||
/** 点了块右上角的按钮:按技能命令重写这块(开新会话),展开它的重写记录 */
|
/** 点了块右上角的按钮:按技能命令重写这块(开新会话),展开它的重写记录 */
|
||||||
onRewrite: (anchor: string) => void
|
onRewrite: (anchor: string) => void
|
||||||
/** 在输入框里说的修改意见,接着这块最近一次重写的会话说;images 是贴的图片 */
|
/** 在输入框里说的修改意见,接着这块最近一次重写的会话说;images 是贴的图片 */
|
||||||
@@ -79,6 +78,7 @@ export interface RewriteTree {
|
|||||||
onDelete: (anchor: string, entryId: string) => void
|
onDelete: (anchor: string, entryId: string) => void
|
||||||
onOpen: (anchor: string) => void
|
onOpen: (anchor: string) => void
|
||||||
onClose: (anchor: string) => void
|
onClose: (anchor: string) => void
|
||||||
|
/** 换重写注释用的 profile(所有块共用),`id` 同 [下拉项的 id](vscode/webview-ui/src/codeReader/types.ts#ProfileOption.id) */
|
||||||
onSelectProfile: (id: string) => void
|
onSelectProfile: (id: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +222,7 @@ function BlockRewrite({ anchor, rewrite }: { anchor: string, rewrite: RewriteTre
|
|||||||
history={historyOf(rewrite.state, anchor)}
|
history={historyOf(rewrite.state, anchor)}
|
||||||
asks={asksOf(rewrite.state, anchor)}
|
asks={asksOf(rewrite.state, anchor)}
|
||||||
open={rewrite.state.open.includes(anchor)}
|
open={rewrite.state.open.includes(anchor)}
|
||||||
profiles={rewrite.profiles}
|
profiles={rewrite.state.profiles}
|
||||||
onSelectProfile={rewrite.onSelectProfile}
|
onSelectProfile={rewrite.onSelectProfile}
|
||||||
onAsk={(request, images) => rewrite.onAsk(anchor, request, images)}
|
onAsk={(request, images) => rewrite.onAsk(anchor, request, images)}
|
||||||
onStop={rewrite.onStop}
|
onStop={rewrite.onStop}
|
||||||
|
|||||||
@@ -34,12 +34,17 @@ describe('问 AI 的状态', () => {
|
|||||||
expect(historyOf(state, IF).map(entry => entry.id)).toEqual(['old'])
|
expect(historyOf(state, IF).map(entry => entry.id)).toEqual(['old'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('profile 下拉:扩展端发来什么就显示什么,问答刷新不丢', () => {
|
it('profile 下拉:问答、重写注释各记各的,扩展端发来什么就显示什么,记录刷新不丢', () => {
|
||||||
const profiles = { options: [{ id: '', label: '默认' }, { id: 'sonnet', label: 'sonnet' }], selected: 'sonnet' }
|
const profiles = { options: [{ id: '', label: '默认' }, { id: 'sonnet', label: 'sonnet' }], selected: 'sonnet' }
|
||||||
expect(EMPTY_QA.profiles).toEqual({ options: [], selected: '' })
|
expect(EMPTY_QA.profiles).toEqual({ options: [], selected: '' })
|
||||||
const state = run({ type: 'profiles', profiles }, qa([thread(IF, 'e1')]))
|
const state = run({ type: 'profiles', profiles }, qa([thread(IF, 'e1')]))
|
||||||
expect(state.profiles).toEqual(profiles)
|
expect(state.profiles).toEqual(profiles)
|
||||||
expect(state.threads).toHaveLength(1)
|
expect(state.threads).toHaveLength(1)
|
||||||
|
|
||||||
|
expect(EMPTY_CHAT.profiles).toEqual({ options: [], selected: '' })
|
||||||
|
const rewrite = chatReducer(chatReducer(EMPTY_CHAT, { type: 'profiles', profiles }), { type: 'sync', available: true, threads: [thread(IF, 'r1')], current: [] })
|
||||||
|
expect(rewrite.profiles).toEqual(profiles)
|
||||||
|
expect(rewrite.threads).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('顶部的问答记在路径上,不进任何一块的历史', () => {
|
it('顶部的问答记在路径上,不进任何一块的历史', () => {
|
||||||
|
|||||||
@@ -37,6 +37,8 @@ export interface ChatState {
|
|||||||
asks: Ask[]
|
asks: Ask[]
|
||||||
/** 展开着对话区(历史、当前这一轮、输入框)的块 */
|
/** 展开着对话区(历史、当前这一轮、输入框)的块 */
|
||||||
open: string[]
|
open: string[]
|
||||||
|
/** 这种对话输入框旁边的 profile 下拉,所有块共用 */
|
||||||
|
profiles: ProfileChoice
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QaState extends ChatState {
|
export interface QaState extends ChatState {
|
||||||
@@ -44,8 +46,6 @@ export interface QaState extends ChatState {
|
|||||||
path: QaEntryView[]
|
path: QaEntryView[]
|
||||||
/** 顶部问的是这个文件、这个文件夹,还是「了解一块代码」找的一个功能 */
|
/** 顶部问的是这个文件、这个文件夹,还是「了解一块代码」找的一个功能 */
|
||||||
subject: 'file' | 'folder' | 'feature'
|
subject: 'file' | 'folder' | 'feature'
|
||||||
/** 输入框旁边的 profile 下拉,所有块共用 */
|
|
||||||
profiles: ProfileChoice
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_CHAT: ChatState = {
|
export const EMPTY_CHAT: ChatState = {
|
||||||
@@ -53,17 +53,18 @@ export const EMPTY_CHAT: ChatState = {
|
|||||||
threads: [],
|
threads: [],
|
||||||
asks: [],
|
asks: [],
|
||||||
open: [],
|
open: [],
|
||||||
|
profiles: { options: [], selected: '' },
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EMPTY_QA: QaState = {
|
export const EMPTY_QA: QaState = {
|
||||||
...EMPTY_CHAT,
|
...EMPTY_CHAT,
|
||||||
path: [],
|
path: [],
|
||||||
subject: 'file',
|
subject: 'file',
|
||||||
profiles: { options: [], selected: '' },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* sync:扩展端发来的记录和它记着的当前一轮;answer:askId 那一轮的回答长了、排上队了(queued)或轮到了(question 是扩展端报上来的这一轮的话,点按钮那一轮开始时页面还不知道)
|
* sync:扩展端发来的记录和它记着的当前一轮;answer:askId 那一轮的回答长了、排上队了(queued)或轮到了(question 是扩展端报上来的这一轮的话,点按钮那一轮开始时页面还不知道);
|
||||||
|
* profiles:扩展端发来的这种对话的 profile 下拉
|
||||||
*/
|
*/
|
||||||
export type ChatAction
|
export type ChatAction
|
||||||
= | { type: 'sync', available: boolean, threads: QaThreadView[], current: AskRound[] }
|
= | { type: 'sync', available: boolean, threads: QaThreadView[], current: AskRound[] }
|
||||||
@@ -72,11 +73,11 @@ export type ChatAction
|
|||||||
| { type: 'ask', askId: string, anchor: string, question: string, images: string[] }
|
| { type: 'ask', askId: string, anchor: string, question: string, images: string[] }
|
||||||
| { type: 'open', anchor: string }
|
| { type: 'open', anchor: string }
|
||||||
| { type: 'close', anchor: string }
|
| { type: 'close', anchor: string }
|
||||||
|
| { type: 'profiles', profiles: ProfileChoice }
|
||||||
|
|
||||||
export type QaAction
|
export type QaAction
|
||||||
= | ChatAction
|
= | ChatAction
|
||||||
| { type: 'qa', message: QaMessage }
|
| { type: 'qa', message: QaMessage }
|
||||||
| { type: 'profiles', profiles: ProfileChoice }
|
|
||||||
|
|
||||||
function withOpen(open: string[], anchor: string): string[] {
|
function withOpen(open: string[], anchor: string): string[] {
|
||||||
return open.includes(anchor) ? open : [...open, anchor]
|
return open.includes(anchor) ? open : [...open, anchor]
|
||||||
@@ -127,6 +128,8 @@ export function chatReducer<S extends ChatState>(state: S, action: ChatAction):
|
|||||||
asks: state.asks.filter(ask => ask.anchor !== action.anchor || unfinished(ask)),
|
asks: state.asks.filter(ask => ask.anchor !== action.anchor || unfinished(ask)),
|
||||||
open: state.open.filter(anchor => anchor !== action.anchor),
|
open: state.open.filter(anchor => anchor !== action.anchor),
|
||||||
}
|
}
|
||||||
|
case 'profiles':
|
||||||
|
return { ...state, profiles: action.profiles }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,8 +139,6 @@ export function qaReducer(state: QaState, action: QaAction): QaState {
|
|||||||
const { available, threads, current, path, subject } = action.message
|
const { available, threads, current, path, subject } = action.message
|
||||||
return { ...chatReducer(state, { type: 'sync', available, threads, current }), path, subject }
|
return { ...chatReducer(state, { type: 'sync', available, threads, current }), path, subject }
|
||||||
}
|
}
|
||||||
case 'profiles':
|
|
||||||
return { ...state, profiles: action.profiles }
|
|
||||||
default:
|
default:
|
||||||
return chatReducer(state, action)
|
return chatReducer(state, action)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,13 +316,16 @@ export interface QaMessage {
|
|||||||
rewriteCurrent: AskRound[]
|
rewriteCurrent: AskRound[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 「问 AI」用的 Claude Code profile 的下拉一项:id 是 profile 名;「默认」(不带 `--settings`)的 id 是 '' */
|
/** 预览里的两种对话:问 AI(ask,块下面和页面顶部)、重写注释(rewrite),各有各的记录和 profile 下拉 */
|
||||||
|
export type ChatKind = 'ask' | 'rewrite'
|
||||||
|
|
||||||
|
/** Claude Code profile 下拉的一项:id 是 profile 名;「默认」(不带 `--settings`)的 id 是 '' */
|
||||||
export interface ProfileOption {
|
export interface ProfileOption {
|
||||||
id: string
|
id: string
|
||||||
label: string
|
label: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 「问 AI」输入框旁边的 profile 下拉:选项和选中的那项,所有预览共用、扩展端记着 */
|
/** 一种对话输入框旁边的 profile 下拉:选项和选中的那项,所有预览共用、扩展端记着 */
|
||||||
export interface ProfileChoice {
|
export interface ProfileChoice {
|
||||||
options: ProfileOption[]
|
options: ProfileOption[]
|
||||||
selected: string
|
selected: string
|
||||||
@@ -460,7 +463,7 @@ export type OpenedFrom = 'uncommitted' | 'other'
|
|||||||
* occurrencesFound:id 那次 findOccurrences 的回答,语言扩展没答(或没装)就是空的
|
* occurrencesFound:id 那次 findOccurrences 的回答,语言扩展没答(或没装)就是空的
|
||||||
* filter:整个工作区共用的块筛选和「只看改动」开关;ready 后先于第一份文档来一次,之后任何一个预览改了都会来
|
* filter:整个工作区共用的块筛选和「只看改动」开关;ready 后先于第一份文档来一次,之后任何一个预览改了都会来
|
||||||
* qa:这份文件的问答;ready 后来一次,问答、正在答的问题变了再来
|
* qa:这份文件的问答;ready 后来一次,问答、正在答的问题变了再来
|
||||||
* askProfiles:「问 AI」的 profile 下拉;ready 后来一次,任何一个预览改了都会来
|
* profiles:kind 那种对话的 profile 下拉;ready 后两种各来一次,任何一个预览改了哪种就来哪种
|
||||||
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
|
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
|
||||||
* answerEnd:askId 那个问题答完了,见 AnswerEnd
|
* answerEnd:askId 那个问题答完了,见 AnswerEnd
|
||||||
* rewriteProgress:askId 那一轮重写排上队了(queued)、开始了、正文长了(text 是到现在为止写出来的)、或换了在做的事;question 是记在这一轮上的话(点按钮时是那句技能命令)
|
* rewriteProgress:askId 那一轮重写排上队了(queued)、开始了、正文长了(text 是到现在为止写出来的)、或换了在做的事;question 是记在这一轮上的话(点按钮时是那句技能命令)
|
||||||
@@ -528,7 +531,7 @@ export type ExtensionToWebview
|
|||||||
| { type: 'displayNames', version: number, names: ShownName[] }
|
| { type: 'displayNames', version: number, names: ShownName[] }
|
||||||
| { type: 'filter', excluded: Excluded, onlyChanges: boolean }
|
| { type: 'filter', excluded: Excluded, onlyChanges: boolean }
|
||||||
| QaMessage
|
| QaMessage
|
||||||
| ({ type: 'askProfiles' } & ProfileChoice)
|
| ({ type: 'profiles', kind: ChatKind } & ProfileChoice)
|
||||||
| { type: 'answer', askId: string, text: string, activity?: string }
|
| { type: 'answer', askId: string, text: string, activity?: string }
|
||||||
| AnswerEnd
|
| AnswerEnd
|
||||||
| { type: 'rewriteProgress', askId: string, question: string, text: string, activity?: string, queued?: true }
|
| { type: 'rewriteProgress', askId: string, question: string, text: string, activity?: string, queued?: true }
|
||||||
@@ -564,7 +567,7 @@ export type ExtensionToWebview
|
|||||||
* forgetRound:收起了 anchor 那块的问答(ask)或重写(rewrite),扩展端去掉这块留着的那一轮(出错、被叫停的),重新加载后不再画出来
|
* forgetRound:收起了 anchor 那块的问答(ask)或重写(rewrite),扩展端去掉这块留着的那一轮(出错、被叫停的),重新加载后不再画出来
|
||||||
* stopAsk:叫停 askId 那个问题,这次不存
|
* stopAsk:叫停 askId 那个问题,这次不存
|
||||||
* deleteQa:删掉 anchor 那块下面的一次问答(已经理解了)
|
* deleteQa:删掉 anchor 那块下面的一次问答(已经理解了)
|
||||||
* selectAskProfile:「问 AI」换 profile;不在本地先改,等扩展端发回 askProfiles 再按它显示
|
* selectProfile:kind 那种对话换 profile;不在本地先改,等扩展端发回这种的 profiles 再按它显示
|
||||||
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发
|
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发
|
||||||
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
||||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),扩展端按这份文件找 symbol
|
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),扩展端按这份文件找 symbol
|
||||||
@@ -581,7 +584,7 @@ export type WebviewToExtension
|
|||||||
| { type: 'rewriteComment', askId: string, anchor: string, request?: string, images: AskImage[] }
|
| { type: 'rewriteComment', askId: string, anchor: string, request?: string, images: AskImage[] }
|
||||||
| { type: 'stopRewrite', askId: string }
|
| { type: 'stopRewrite', askId: string }
|
||||||
| { type: 'deleteRewrite', anchor: string, entryId: string }
|
| { type: 'deleteRewrite', anchor: string, entryId: string }
|
||||||
| { type: 'forgetRound', kind: 'ask' | 'rewrite', anchor: string }
|
| { type: 'forgetRound', kind: ChatKind, anchor: string }
|
||||||
| { type: 'selectTranslator', id: string }
|
| { type: 'selectTranslator', id: string }
|
||||||
| { type: 'visibleLine', line: number }
|
| { type: 'visibleLine', line: number }
|
||||||
| { type: 'probeDefinition', id: number, version: number, offset: number }
|
| { type: 'probeDefinition', id: number, version: number, offset: number }
|
||||||
@@ -592,7 +595,7 @@ export type WebviewToExtension
|
|||||||
| { type: 'ask', askId: string, anchor: string, question: string, images: AskImage[] }
|
| { type: 'ask', askId: string, anchor: string, question: string, images: AskImage[] }
|
||||||
| { type: 'stopAsk', askId: string }
|
| { type: 'stopAsk', askId: string }
|
||||||
| { type: 'deleteQa', anchor: string, entryId: string }
|
| { type: 'deleteQa', anchor: string, entryId: string }
|
||||||
| { type: 'selectAskProfile', id: string }
|
| { type: 'selectProfile', kind: ChatKind, id: string }
|
||||||
| { type: 'occurrenceNavigation', available: boolean }
|
| { type: 'occurrenceNavigation', available: boolean }
|
||||||
| { type: 'openRepoLink', path?: string, symbol?: string }
|
| { type: 'openRepoLink', path?: string, symbol?: string }
|
||||||
| { type: 'checkRepoLinks', id: number, paths: string[], symbols: string[] }
|
| { type: 'checkRepoLinks', id: number, paths: string[], symbols: string[] }
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export function useQa(): QaTree {
|
|||||||
case 'qa':
|
case 'qa':
|
||||||
dispatch({ type: 'qa', message: msg })
|
dispatch({ type: 'qa', message: msg })
|
||||||
return
|
return
|
||||||
case 'askProfiles':
|
case 'profiles':
|
||||||
|
if (msg.kind === 'ask')
|
||||||
dispatch({ type: 'profiles', profiles: { options: msg.options, selected: msg.selected } })
|
dispatch({ type: 'profiles', profiles: { options: msg.options, selected: msg.selected } })
|
||||||
return
|
return
|
||||||
case 'answer':
|
case 'answer':
|
||||||
@@ -66,7 +67,7 @@ export function useQa(): QaTree {
|
|||||||
},
|
},
|
||||||
onStop: askId => postMessage({ type: 'stopAsk', askId }),
|
onStop: askId => postMessage({ type: 'stopAsk', askId }),
|
||||||
onDelete: (anchor, entryId) => postMessage({ type: 'deleteQa', anchor, entryId }),
|
onDelete: (anchor, entryId) => postMessage({ type: 'deleteQa', anchor, entryId }),
|
||||||
onSelectProfile: id => postMessage({ type: 'selectAskProfile', id }),
|
onSelectProfile: id => postMessage({ type: 'selectProfile', kind: 'ask', id }),
|
||||||
}
|
}
|
||||||
}, [state])
|
}, [state])
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { RewriteTree } from './BlockTree'
|
import type { RewriteTree } from './BlockTree'
|
||||||
import type { ChatState } from './qa'
|
import type { ChatState } from './qa'
|
||||||
import type { ProfileChoice } from './types'
|
|
||||||
import { useEffect, useMemo, useReducer, useRef } from 'react'
|
import { useEffect, useMemo, useReducer, useRef } from 'react'
|
||||||
import { chatReducer, EMPTY_CHAT, newAskId, unfinished } from './qa'
|
import { chatReducer, EMPTY_CHAT, newAskId, unfinished } from './qa'
|
||||||
import { toAskImage } from './qaImages'
|
import { toAskImage } from './qaImages'
|
||||||
@@ -9,9 +8,9 @@ import { onMessage, postMessage } from './vscode'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 按注释规范重写:收扩展端的重写记录、当前一轮的进展,给块树用的状态和动作。和问 AI 同一套:
|
* 按注释规范重写:收扩展端的重写记录、当前一轮的进展,给块树用的状态和动作。和问 AI 同一套:
|
||||||
* 排着队的、进行中的、出错被叫停留着的那一轮记在扩展端,页面重新加载后随 qa 消息的 rewriteCurrent 接上。profile 下拉和问 AI 共用。
|
* 排着队的、进行中的、出错被叫停留着的那一轮记在扩展端,页面重新加载后随 qa 消息的 rewriteCurrent 接上。profile 下拉是自己的,和问 AI 的互不影响。
|
||||||
*/
|
*/
|
||||||
export function useRewrite(profiles: ProfileChoice, onSelectProfile: (id: string) => void): RewriteTree {
|
export function useRewrite(): RewriteTree {
|
||||||
const [state, dispatch] = useReducer(chatReducer<ChatState>, EMPTY_CHAT)
|
const [state, dispatch] = useReducer(chatReducer<ChatState>, EMPTY_CHAT)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -29,6 +28,10 @@ export function useRewrite(profiles: ProfileChoice, onSelectProfile: (id: string
|
|||||||
case 'qa':
|
case 'qa':
|
||||||
dispatch({ type: 'sync', available: msg.available, threads: msg.rewrites, current: msg.rewriteCurrent })
|
dispatch({ type: 'sync', available: msg.available, threads: msg.rewrites, current: msg.rewriteCurrent })
|
||||||
return
|
return
|
||||||
|
case 'profiles':
|
||||||
|
if (msg.kind === 'rewrite')
|
||||||
|
dispatch({ type: 'profiles', profiles: { options: msg.options, selected: msg.selected } })
|
||||||
|
return
|
||||||
case 'rewriteProgress':
|
case 'rewriteProgress':
|
||||||
pending.set(msg.askId, { question: msg.question, text: msg.text, ...(msg.activity !== undefined && { activity: msg.activity }), ...(msg.queued && { queued: true }) })
|
pending.set(msg.askId, { question: msg.question, text: msg.text, ...(msg.activity !== undefined && { activity: msg.activity }), ...(msg.queued && { queued: true }) })
|
||||||
timer ??= setTimeout(flush, ANSWER_FRAME_MS)
|
timer ??= setTimeout(flush, ANSWER_FRAME_MS)
|
||||||
@@ -51,8 +54,7 @@ export function useRewrite(profiles: ProfileChoice, onSelectProfile: (id: string
|
|||||||
|
|
||||||
return useMemo((): RewriteTree => ({
|
return useMemo((): RewriteTree => ({
|
||||||
state,
|
state,
|
||||||
profiles,
|
onSelectProfile: id => postMessage({ type: 'selectProfile', kind: 'rewrite', id }),
|
||||||
onSelectProfile,
|
|
||||||
onRewrite: (anchor) => {
|
onRewrite: (anchor) => {
|
||||||
if (current.current.asks.some(ask => ask.anchor === anchor && unfinished(ask)))
|
if (current.current.asks.some(ask => ask.anchor === anchor && unfinished(ask)))
|
||||||
return
|
return
|
||||||
@@ -73,5 +75,5 @@ export function useRewrite(profiles: ProfileChoice, onSelectProfile: (id: string
|
|||||||
dispatch({ type: 'close', anchor })
|
dispatch({ type: 'close', anchor })
|
||||||
postMessage({ type: 'forgetRound', kind: 'rewrite', anchor })
|
postMessage({ type: 'forgetRound', kind: 'rewrite', anchor })
|
||||||
},
|
},
|
||||||
}), [state, profiles, onSelectProfile])
|
}), [state])
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ const PROPS: SettingsModalProps = {
|
|||||||
initialDefaultTestProfilePath: '',
|
initialDefaultTestProfilePath: '',
|
||||||
initialDefaultPrototypeProfilePath: '',
|
initialDefaultPrototypeProfilePath: '',
|
||||||
initialDefaultAskProfilePath: '/p/sonnet.json',
|
initialDefaultAskProfilePath: '/p/sonnet.json',
|
||||||
|
initialDefaultRewriteProfilePath: '/p/dsflash.json',
|
||||||
initialConflictResolutionProfilePath: '',
|
initialConflictResolutionProfilePath: '',
|
||||||
initialCommitProfilePath: '',
|
initialCommitProfilePath: '',
|
||||||
initialPrDiffSummaryProfilePath: '',
|
initialPrDiffSummaryProfilePath: '',
|
||||||
@@ -42,30 +43,52 @@ const PROPS: SettingsModalProps = {
|
|||||||
initialYoutrackProjectShortName: '',
|
initialYoutrackProjectShortName: '',
|
||||||
initialYoutrackCloseCommand: '',
|
initialYoutrackCloseCommand: '',
|
||||||
initialYoutrackTokenSaved: false,
|
initialYoutrackTokenSaved: false,
|
||||||
profiles: [{ name: 'sonnet', path: '/p/sonnet.json' }],
|
profiles: [{ name: 'sonnet', path: '/p/sonnet.json' }, { name: 'dsflash', path: '/p/dsflash.json' }],
|
||||||
youtrackProjects: { status: 'idle', projects: [] },
|
youtrackProjects: { status: 'idle', projects: [] },
|
||||||
onListYouTrackProjects: () => {},
|
onListYouTrackProjects: () => {},
|
||||||
onSubmit: () => {},
|
onSubmit: () => {},
|
||||||
}
|
}
|
||||||
|
|
||||||
function field(props: Partial<SettingsModalProps> = {}): string {
|
/** 设置面板里 from 这一项到 to 那一项之间的那一段 */
|
||||||
|
function between(props: Partial<SettingsModalProps>, from: string, to: string): string {
|
||||||
const html = renderToStaticMarkup(<SettingsModal {...PROPS} {...props} />)
|
const html = renderToStaticMarkup(<SettingsModal {...PROPS} {...props} />)
|
||||||
expect(html).toContain('文件问答默认模型')
|
expect(html).toContain(from)
|
||||||
return html.slice(html.indexOf('文件问答默认模型'), html.indexOf('冲突解决配置文件'))
|
return html.slice(html.indexOf(from), html.indexOf(to))
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('设置面板的文件问答默认模型', () => {
|
const askField = (props: Partial<SettingsModalProps> = {}) => between(props, '问答默认模型', '重写注释默认模型')
|
||||||
|
const rewriteField = (props: Partial<SettingsModalProps> = {}) => between(props, '重写注释默认模型', '冲突解决配置文件')
|
||||||
|
|
||||||
|
describe('设置面板的问答默认模型', () => {
|
||||||
it('与其他默认配置在同一区域,显示保存的名称并说明临时切换不会改默认', () => {
|
it('与其他默认配置在同一区域,显示保存的名称并说明临时切换不会改默认', () => {
|
||||||
const html = field()
|
const html = askField()
|
||||||
expect(html).toContain('sonnet')
|
expect(html).toContain('sonnet')
|
||||||
|
expect(html).not.toContain('dsflash')
|
||||||
expect(html).toContain('问这个文件')
|
expect(html).toContain('问这个文件')
|
||||||
|
expect(html).toContain('了解一块代码')
|
||||||
expect(html).toContain('临时切换')
|
expect(html).toContain('临时切换')
|
||||||
expect(html).toContain('不会修改这里的默认值')
|
expect(html).toContain('不会修改这里的默认值')
|
||||||
expect(html).toContain('aria-haspopup="listbox"')
|
expect(html).toContain('aria-haspopup="listbox"')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('未配置时显示官方默认,失效路径不能伪装成默认', () => {
|
it('未配置时显示官方默认,失效路径不能伪装成默认', () => {
|
||||||
expect(field({ initialDefaultAskProfilePath: '' })).toContain('(默认)')
|
expect(askField({ initialDefaultAskProfilePath: '' })).toContain('(默认)')
|
||||||
expect(field({ initialDefaultAskProfilePath: '/p/deleted.json' })).toContain('不可用:/p/deleted.json')
|
expect(askField({ initialDefaultAskProfilePath: '/p/deleted.json' })).toContain('不可用:/p/deleted.json')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('设置面板的重写注释默认模型', () => {
|
||||||
|
it('紧挨着问答默认模型,显示自己保存的名称,说明临时切换不会改默认', () => {
|
||||||
|
const html = rewriteField()
|
||||||
|
expect(html).toContain('dsflash')
|
||||||
|
expect(html).toContain('重写')
|
||||||
|
expect(html).toContain('临时切换')
|
||||||
|
expect(html).toContain('不会修改这里的默认值')
|
||||||
|
expect(html).toContain('aria-haspopup="listbox"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('未配置时显示官方默认,失效路径不能伪装成默认', () => {
|
||||||
|
expect(rewriteField({ initialDefaultRewriteProfilePath: '' })).toContain('(默认)')
|
||||||
|
expect(rewriteField({ initialDefaultRewriteProfilePath: '/p/deleted.json' })).toContain('不可用:/p/deleted.json')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -43,6 +43,7 @@ interface SubmitValues {
|
|||||||
defaultTestProfilePath: string
|
defaultTestProfilePath: string
|
||||||
defaultPrototypeProfilePath: string
|
defaultPrototypeProfilePath: string
|
||||||
defaultAskProfilePath: string
|
defaultAskProfilePath: string
|
||||||
|
defaultRewriteProfilePath: string
|
||||||
conflictResolutionProfilePath: string
|
conflictResolutionProfilePath: string
|
||||||
commitProfilePath: string
|
commitProfilePath: string
|
||||||
prDiffSummaryProfilePath: string
|
prDiffSummaryProfilePath: string
|
||||||
@@ -99,6 +100,7 @@ export interface SettingsModalProps {
|
|||||||
initialDefaultTestProfilePath: string
|
initialDefaultTestProfilePath: string
|
||||||
initialDefaultPrototypeProfilePath: string
|
initialDefaultPrototypeProfilePath: string
|
||||||
initialDefaultAskProfilePath: string
|
initialDefaultAskProfilePath: string
|
||||||
|
initialDefaultRewriteProfilePath: string
|
||||||
initialConflictResolutionProfilePath: string
|
initialConflictResolutionProfilePath: string
|
||||||
initialCommitProfilePath: string
|
initialCommitProfilePath: string
|
||||||
initialPrDiffSummaryProfilePath: string
|
initialPrDiffSummaryProfilePath: string
|
||||||
@@ -192,6 +194,7 @@ export function SettingsModal({
|
|||||||
initialDefaultTestProfilePath,
|
initialDefaultTestProfilePath,
|
||||||
initialDefaultPrototypeProfilePath,
|
initialDefaultPrototypeProfilePath,
|
||||||
initialDefaultAskProfilePath,
|
initialDefaultAskProfilePath,
|
||||||
|
initialDefaultRewriteProfilePath,
|
||||||
initialConflictResolutionProfilePath,
|
initialConflictResolutionProfilePath,
|
||||||
initialCommitProfilePath,
|
initialCommitProfilePath,
|
||||||
initialPrDiffSummaryProfilePath,
|
initialPrDiffSummaryProfilePath,
|
||||||
@@ -236,6 +239,7 @@ export function SettingsModal({
|
|||||||
const [defaultTestProfilePath, setDefaultTestProfilePath] = useState(initialDefaultTestProfilePath)
|
const [defaultTestProfilePath, setDefaultTestProfilePath] = useState(initialDefaultTestProfilePath)
|
||||||
const [defaultPrototypeProfilePath, setDefaultPrototypeProfilePath] = useState(initialDefaultPrototypeProfilePath)
|
const [defaultPrototypeProfilePath, setDefaultPrototypeProfilePath] = useState(initialDefaultPrototypeProfilePath)
|
||||||
const [defaultAskProfilePath, setDefaultAskProfilePath] = useState(initialDefaultAskProfilePath)
|
const [defaultAskProfilePath, setDefaultAskProfilePath] = useState(initialDefaultAskProfilePath)
|
||||||
|
const [defaultRewriteProfilePath, setDefaultRewriteProfilePath] = useState(initialDefaultRewriteProfilePath)
|
||||||
const [conflictResolutionProfilePath, setConflictResolutionProfilePath] = useState(initialConflictResolutionProfilePath)
|
const [conflictResolutionProfilePath, setConflictResolutionProfilePath] = useState(initialConflictResolutionProfilePath)
|
||||||
const [commitProfilePath, setCommitProfilePath] = useState(initialCommitProfilePath)
|
const [commitProfilePath, setCommitProfilePath] = useState(initialCommitProfilePath)
|
||||||
const [prDiffSummaryProfilePath, setPrDiffSummaryProfilePath] = useState(initialPrDiffSummaryProfilePath)
|
const [prDiffSummaryProfilePath, setPrDiffSummaryProfilePath] = useState(initialPrDiffSummaryProfilePath)
|
||||||
@@ -282,6 +286,7 @@ export function SettingsModal({
|
|||||||
setDefaultTestProfilePath(initialDefaultTestProfilePath)
|
setDefaultTestProfilePath(initialDefaultTestProfilePath)
|
||||||
setDefaultPrototypeProfilePath(initialDefaultPrototypeProfilePath)
|
setDefaultPrototypeProfilePath(initialDefaultPrototypeProfilePath)
|
||||||
setDefaultAskProfilePath(initialDefaultAskProfilePath)
|
setDefaultAskProfilePath(initialDefaultAskProfilePath)
|
||||||
|
setDefaultRewriteProfilePath(initialDefaultRewriteProfilePath)
|
||||||
setConflictResolutionProfilePath(initialConflictResolutionProfilePath)
|
setConflictResolutionProfilePath(initialConflictResolutionProfilePath)
|
||||||
setCommitProfilePath(initialCommitProfilePath)
|
setCommitProfilePath(initialCommitProfilePath)
|
||||||
setPrDiffSummaryProfilePath(initialPrDiffSummaryProfilePath)
|
setPrDiffSummaryProfilePath(initialPrDiffSummaryProfilePath)
|
||||||
@@ -380,6 +385,7 @@ export function SettingsModal({
|
|||||||
defaultTestProfilePath: defaultTestProfilePath.trim(),
|
defaultTestProfilePath: defaultTestProfilePath.trim(),
|
||||||
defaultPrototypeProfilePath: defaultPrototypeProfilePath.trim(),
|
defaultPrototypeProfilePath: defaultPrototypeProfilePath.trim(),
|
||||||
defaultAskProfilePath: defaultAskProfilePath.trim(),
|
defaultAskProfilePath: defaultAskProfilePath.trim(),
|
||||||
|
defaultRewriteProfilePath: defaultRewriteProfilePath.trim(),
|
||||||
// 冲突解决 profile:空串 = 用 DEFAULT_PROFILE_PATH
|
// 冲突解决 profile:空串 = 用 DEFAULT_PROFILE_PATH
|
||||||
conflictResolutionProfilePath: conflictResolutionProfilePath.trim(),
|
conflictResolutionProfilePath: conflictResolutionProfilePath.trim(),
|
||||||
// 提交代码 profile:空串 = 未配置 → 提交严格失败
|
// 提交代码 profile:空串 = 未配置 → 提交严格失败
|
||||||
@@ -663,8 +669,8 @@ export function SettingsModal({
|
|||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label="文件问答默认模型"
|
label="问答默认模型"
|
||||||
hint="代码阅读底部「问这个文件」默认使用此模型,也用于代码块和文件夹问答;留空沿用官方默认。输入框旁可临时切换,不会修改这里的默认值;重启或修改此设置后恢复默认。"
|
hint="代码阅读里问 AI 默认使用此模型:代码块下面的问答、预览顶部问这个文件或文件夹、「了解一块代码」都用它;留空沿用官方默认。输入框旁可临时切换,不会修改这里的默认值;重启或修改此设置后恢复默认。"
|
||||||
>
|
>
|
||||||
<SelectMenu
|
<SelectMenu
|
||||||
value={defaultAskProfilePath}
|
value={defaultAskProfilePath}
|
||||||
@@ -682,6 +688,26 @@ export function SettingsModal({
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="重写注释默认模型"
|
||||||
|
hint="代码阅读里重写注释默认使用此模型:点按钮重写、在输入框里接着说都用它;留空沿用官方默认。输入框旁可临时切换,不会修改这里的默认值;重启或修改此设置后恢复默认。"
|
||||||
|
>
|
||||||
|
<SelectMenu
|
||||||
|
value={defaultRewriteProfilePath}
|
||||||
|
onChange={setDefaultRewriteProfilePath}
|
||||||
|
variant="input"
|
||||||
|
className={inputClass}
|
||||||
|
options={[
|
||||||
|
{ value: '', label: '(默认)' },
|
||||||
|
...profiles.map(p => ({ value: p.path, label: p.name })),
|
||||||
|
...(defaultRewriteProfilePath
|
||||||
|
&& !profiles.some(p => p.path === defaultRewriteProfilePath)
|
||||||
|
? [{ value: defaultRewriteProfilePath, label: `不可用:${defaultRewriteProfilePath}` }]
|
||||||
|
: []),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
<Field
|
<Field
|
||||||
label="冲突解决配置文件"
|
label="冲突解决配置文件"
|
||||||
hint={(
|
hint={(
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export interface SettingsValues {
|
|||||||
defaultTestProfilePath: string
|
defaultTestProfilePath: string
|
||||||
defaultPrototypeProfilePath: string
|
defaultPrototypeProfilePath: string
|
||||||
defaultAskProfilePath: string
|
defaultAskProfilePath: string
|
||||||
|
defaultRewriteProfilePath: string
|
||||||
conflictResolutionProfilePath: string
|
conflictResolutionProfilePath: string
|
||||||
commitProfilePath: string
|
commitProfilePath: string
|
||||||
prDiffSummaryProfilePath: string
|
prDiffSummaryProfilePath: string
|
||||||
@@ -95,6 +96,7 @@ export interface SettingsOverlayState {
|
|||||||
defaultTestProfilePath: string
|
defaultTestProfilePath: string
|
||||||
defaultPrototypeProfilePath: string
|
defaultPrototypeProfilePath: string
|
||||||
defaultAskProfilePath: string
|
defaultAskProfilePath: string
|
||||||
|
defaultRewriteProfilePath: string
|
||||||
conflictResolutionProfilePath: string
|
conflictResolutionProfilePath: string
|
||||||
commitProfilePath: string
|
commitProfilePath: string
|
||||||
prDiffSummaryProfilePath: string
|
prDiffSummaryProfilePath: string
|
||||||
@@ -459,6 +461,7 @@ export function useIssues(): UseIssuesResult {
|
|||||||
defaultTestProfilePath: values.defaultTestProfilePath,
|
defaultTestProfilePath: values.defaultTestProfilePath,
|
||||||
defaultPrototypeProfilePath: values.defaultPrototypeProfilePath,
|
defaultPrototypeProfilePath: values.defaultPrototypeProfilePath,
|
||||||
defaultAskProfilePath: values.defaultAskProfilePath,
|
defaultAskProfilePath: values.defaultAskProfilePath,
|
||||||
|
defaultRewriteProfilePath: values.defaultRewriteProfilePath,
|
||||||
conflictResolutionProfilePath: values.conflictResolutionProfilePath,
|
conflictResolutionProfilePath: values.conflictResolutionProfilePath,
|
||||||
commitProfilePath: values.commitProfilePath,
|
commitProfilePath: values.commitProfilePath,
|
||||||
prDiffSummaryProfilePath: values.prDiffSummaryProfilePath,
|
prDiffSummaryProfilePath: values.prDiffSummaryProfilePath,
|
||||||
@@ -981,6 +984,7 @@ export function useIssues(): UseIssuesResult {
|
|||||||
defaultTestProfilePath: msg.defaultTestProfilePath,
|
defaultTestProfilePath: msg.defaultTestProfilePath,
|
||||||
defaultPrototypeProfilePath: msg.defaultPrototypeProfilePath,
|
defaultPrototypeProfilePath: msg.defaultPrototypeProfilePath,
|
||||||
defaultAskProfilePath: msg.defaultAskProfilePath,
|
defaultAskProfilePath: msg.defaultAskProfilePath,
|
||||||
|
defaultRewriteProfilePath: msg.defaultRewriteProfilePath,
|
||||||
conflictResolutionProfilePath: msg.conflictResolutionProfilePath,
|
conflictResolutionProfilePath: msg.conflictResolutionProfilePath,
|
||||||
commitProfilePath: msg.commitProfilePath,
|
commitProfilePath: msg.commitProfilePath,
|
||||||
prDiffSummaryProfilePath: msg.prDiffSummaryProfilePath,
|
prDiffSummaryProfilePath: msg.prDiffSummaryProfilePath,
|
||||||
|
|||||||
@@ -135,6 +135,7 @@ export type ExtensionToWebview
|
|||||||
defaultTestProfilePath: string
|
defaultTestProfilePath: string
|
||||||
defaultPrototypeProfilePath: string
|
defaultPrototypeProfilePath: string
|
||||||
defaultAskProfilePath: string
|
defaultAskProfilePath: string
|
||||||
|
defaultRewriteProfilePath: string
|
||||||
conflictResolutionProfilePath: string
|
conflictResolutionProfilePath: string
|
||||||
commitProfilePath: string
|
commitProfilePath: string
|
||||||
prDiffSummaryProfilePath: string
|
prDiffSummaryProfilePath: string
|
||||||
@@ -221,6 +222,7 @@ export type WebviewToExtension
|
|||||||
defaultTestProfilePath: string
|
defaultTestProfilePath: string
|
||||||
defaultPrototypeProfilePath: string
|
defaultPrototypeProfilePath: string
|
||||||
defaultAskProfilePath: string
|
defaultAskProfilePath: string
|
||||||
|
defaultRewriteProfilePath: string
|
||||||
conflictResolutionProfilePath: string
|
conflictResolutionProfilePath: string
|
||||||
commitProfilePath: string
|
commitProfilePath: string
|
||||||
prDiffSummaryProfilePath: string
|
prDiffSummaryProfilePath: string
|
||||||
|
|||||||
Reference in new issue
Block a user