✨ feat(vscode): 问 AI 和重写注释各用各的 profile,设置里加重写注释默认模型
This commit is contained in:
1 parent
925f1ed39a
commit
b83c445dd0
25 files changed
+363
-111
No files matched your search
@@ -59,8 +59,8 @@ export function App(): ReactElement | null {
|
||||
const qa = useQa()
|
||||
/** 侧栏「问 AI」要聚焦顶部输入框时加一 */
|
||||
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 [view, setView] = useState<BlockModes>(savedBlockModes)
|
||||
const [focusId, setFocusId] = useState<string | null>(null)
|
||||
|
||||
@@ -62,7 +62,7 @@ const CARD: BlockView = {
|
||||
|
||||
/** 重写注释的状态和动作:动作什么都不做 */
|
||||
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 {
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { DiffTree, FoldedRuns, PlacedRemoved } from './diffView'
|
||||
import type { GridSlots } from './PropertyGrid'
|
||||
import type { ChatState, QaState } from './qa'
|
||||
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 { Fragment, useState } from 'react'
|
||||
import { AskThread, REWRITE_THREAD } from './AskThread'
|
||||
@@ -67,10 +67,9 @@ export interface BlockTreeState {
|
||||
onOpenRun: (id: string) => void
|
||||
}
|
||||
|
||||
/** 重写注释的状态和动作,块按自己的记号(anchor)取;profile 下拉和问 AI 共用 */
|
||||
/** 重写注释的状态和动作,块按自己的记号(anchor)取;profile 下拉是自己的,和问 AI 的互不影响 */
|
||||
export interface RewriteTree {
|
||||
state: ChatState
|
||||
profiles: ProfileChoice
|
||||
/** 点了块右上角的按钮:按技能命令重写这块(开新会话),展开它的重写记录 */
|
||||
onRewrite: (anchor: string) => void
|
||||
/** 在输入框里说的修改意见,接着这块最近一次重写的会话说;images 是贴的图片 */
|
||||
@@ -79,6 +78,7 @@ export interface RewriteTree {
|
||||
onDelete: (anchor: string, entryId: string) => void
|
||||
onOpen: (anchor: string) => void
|
||||
onClose: (anchor: string) => void
|
||||
/** 换重写注释用的 profile(所有块共用),`id` 同 [下拉项的 id](vscode/webview-ui/src/codeReader/types.ts#ProfileOption.id) */
|
||||
onSelectProfile: (id: string) => void
|
||||
}
|
||||
|
||||
@@ -222,7 +222,7 @@ function BlockRewrite({ anchor, rewrite }: { anchor: string, rewrite: RewriteTre
|
||||
history={historyOf(rewrite.state, anchor)}
|
||||
asks={asksOf(rewrite.state, anchor)}
|
||||
open={rewrite.state.open.includes(anchor)}
|
||||
profiles={rewrite.profiles}
|
||||
profiles={rewrite.state.profiles}
|
||||
onSelectProfile={rewrite.onSelectProfile}
|
||||
onAsk={(request, images) => rewrite.onAsk(anchor, request, images)}
|
||||
onStop={rewrite.onStop}
|
||||
|
||||
@@ -34,12 +34,17 @@ describe('问 AI 的状态', () => {
|
||||
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' }
|
||||
expect(EMPTY_QA.profiles).toEqual({ options: [], selected: '' })
|
||||
const state = run({ type: 'profiles', profiles }, qa([thread(IF, 'e1')]))
|
||||
expect(state.profiles).toEqual(profiles)
|
||||
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('顶部的问答记在路径上,不进任何一块的历史', () => {
|
||||
|
||||
@@ -37,6 +37,8 @@ export interface ChatState {
|
||||
asks: Ask[]
|
||||
/** 展开着对话区(历史、当前这一轮、输入框)的块 */
|
||||
open: string[]
|
||||
/** 这种对话输入框旁边的 profile 下拉,所有块共用 */
|
||||
profiles: ProfileChoice
|
||||
}
|
||||
|
||||
export interface QaState extends ChatState {
|
||||
@@ -44,8 +46,6 @@ export interface QaState extends ChatState {
|
||||
path: QaEntryView[]
|
||||
/** 顶部问的是这个文件、这个文件夹,还是「了解一块代码」找的一个功能 */
|
||||
subject: 'file' | 'folder' | 'feature'
|
||||
/** 输入框旁边的 profile 下拉,所有块共用 */
|
||||
profiles: ProfileChoice
|
||||
}
|
||||
|
||||
export const EMPTY_CHAT: ChatState = {
|
||||
@@ -53,17 +53,18 @@ export const EMPTY_CHAT: ChatState = {
|
||||
threads: [],
|
||||
asks: [],
|
||||
open: [],
|
||||
profiles: { options: [], selected: '' },
|
||||
}
|
||||
|
||||
export const EMPTY_QA: QaState = {
|
||||
...EMPTY_CHAT,
|
||||
path: [],
|
||||
subject: 'file',
|
||||
profiles: { options: [], selected: '' },
|
||||
}
|
||||
|
||||
/**
|
||||
* sync:扩展端发来的记录和它记着的当前一轮;answer:askId 那一轮的回答长了、排上队了(queued)或轮到了(question 是扩展端报上来的这一轮的话,点按钮那一轮开始时页面还不知道)
|
||||
* sync:扩展端发来的记录和它记着的当前一轮;answer:askId 那一轮的回答长了、排上队了(queued)或轮到了(question 是扩展端报上来的这一轮的话,点按钮那一轮开始时页面还不知道);
|
||||
* profiles:扩展端发来的这种对话的 profile 下拉
|
||||
*/
|
||||
export type ChatAction
|
||||
= | { 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: 'open', anchor: string }
|
||||
| { type: 'close', anchor: string }
|
||||
| { type: 'profiles', profiles: ProfileChoice }
|
||||
|
||||
export type QaAction
|
||||
= | ChatAction
|
||||
| { type: 'qa', message: QaMessage }
|
||||
| { type: 'profiles', profiles: ProfileChoice }
|
||||
|
||||
function withOpen(open: string[], anchor: string): string[] {
|
||||
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)),
|
||||
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
|
||||
return { ...chatReducer(state, { type: 'sync', available, threads, current }), path, subject }
|
||||
}
|
||||
case 'profiles':
|
||||
return { ...state, profiles: action.profiles }
|
||||
default:
|
||||
return chatReducer(state, action)
|
||||
}
|
||||
|
||||
@@ -316,13 +316,16 @@ export interface QaMessage {
|
||||
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 {
|
||||
id: string
|
||||
label: string
|
||||
}
|
||||
|
||||
/** 「问 AI」输入框旁边的 profile 下拉:选项和选中的那项,所有预览共用、扩展端记着 */
|
||||
/** 一种对话输入框旁边的 profile 下拉:选项和选中的那项,所有预览共用、扩展端记着 */
|
||||
export interface ProfileChoice {
|
||||
options: ProfileOption[]
|
||||
selected: string
|
||||
@@ -460,7 +463,7 @@ export type OpenedFrom = 'uncommitted' | 'other'
|
||||
* occurrencesFound:id 那次 findOccurrences 的回答,语言扩展没答(或没装)就是空的
|
||||
* filter:整个工作区共用的块筛选和「只看改动」开关;ready 后先于第一份文档来一次,之后任何一个预览改了都会来
|
||||
* qa:这份文件的问答;ready 后来一次,问答、正在答的问题变了再来
|
||||
* askProfiles:「问 AI」的 profile 下拉;ready 后来一次,任何一个预览改了都会来
|
||||
* profiles:kind 那种对话的 profile 下拉;ready 后两种各来一次,任何一个预览改了哪种就来哪种
|
||||
* answer:askId 那个问题的回答长了(text 是到现在为止的全文)、或换了在做的事
|
||||
* answerEnd:askId 那个问题答完了,见 AnswerEnd
|
||||
* rewriteProgress:askId 那一轮重写排上队了(queued)、开始了、正文长了(text 是到现在为止写出来的)、或换了在做的事;question 是记在这一轮上的话(点按钮时是那句技能命令)
|
||||
@@ -528,7 +531,7 @@ export type ExtensionToWebview
|
||||
| { type: 'displayNames', version: number, names: ShownName[] }
|
||||
| { type: 'filter', excluded: Excluded, onlyChanges: boolean }
|
||||
| QaMessage
|
||||
| ({ type: 'askProfiles' } & ProfileChoice)
|
||||
| ({ type: 'profiles', kind: ChatKind } & ProfileChoice)
|
||||
| { type: 'answer', askId: string, text: string, activity?: string }
|
||||
| AnswerEnd
|
||||
| { type: 'rewriteProgress', askId: string, question: string, text: string, activity?: string, queued?: true }
|
||||
@@ -564,7 +567,7 @@ export type ExtensionToWebview
|
||||
* forgetRound:收起了 anchor 那块的问答(ask)或重写(rewrite),扩展端去掉这块留着的那一轮(出错、被叫停的),重新加载后不再画出来
|
||||
* stopAsk:叫停 askId 那个问题,这次不存
|
||||
* deleteQa:删掉 anchor 那块下面的一次问答(已经理解了)
|
||||
* selectAskProfile:「问 AI」换 profile;不在本地先改,等扩展端发回 askProfiles 再按它显示
|
||||
* selectProfile:kind 那种对话换 profile;不在本地先改,等扩展端发回这种的 profiles 再按它显示
|
||||
* occurrenceNavigation:眼下能不能用键盘在高亮着的名字的各处出现之间跳(有高亮、页面有焦点、焦点不在输入框里),变了才发
|
||||
* openRepoLink:点了注释里的仓库路径链接,打开 path(从仓库根算起)。symbol 是类名、函数名或「类名.成员名」,没有就只打开文件或在代码阅读侧栏的树上定位目录;
|
||||
* 没有 path 是不带路径的引用(`[Name]`、`[Class.member]`),扩展端按这份文件找 symbol
|
||||
@@ -581,7 +584,7 @@ export type WebviewToExtension
|
||||
| { type: 'rewriteComment', askId: string, anchor: string, request?: string, images: AskImage[] }
|
||||
| { type: 'stopRewrite', askId: 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: 'visibleLine', line: 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: 'stopAsk', askId: string }
|
||||
| { type: 'deleteQa', anchor: string, entryId: string }
|
||||
| { type: 'selectAskProfile', id: string }
|
||||
| { type: 'selectProfile', kind: ChatKind, id: string }
|
||||
| { type: 'occurrenceNavigation', available: boolean }
|
||||
| { type: 'openRepoLink', path?: string, symbol?: string }
|
||||
| { type: 'checkRepoLinks', id: number, paths: string[], symbols: string[] }
|
||||
|
||||
@@ -30,8 +30,9 @@ export function useQa(): QaTree {
|
||||
case 'qa':
|
||||
dispatch({ type: 'qa', message: msg })
|
||||
return
|
||||
case 'askProfiles':
|
||||
dispatch({ type: 'profiles', profiles: { options: msg.options, selected: msg.selected } })
|
||||
case 'profiles':
|
||||
if (msg.kind === 'ask')
|
||||
dispatch({ type: 'profiles', profiles: { options: msg.options, selected: msg.selected } })
|
||||
return
|
||||
case 'answer':
|
||||
pending.set(msg.askId, { text: msg.text, activity: msg.activity })
|
||||
@@ -66,7 +67,7 @@ export function useQa(): QaTree {
|
||||
},
|
||||
onStop: askId => postMessage({ type: 'stopAsk', askId }),
|
||||
onDelete: (anchor, entryId) => postMessage({ type: 'deleteQa', anchor, entryId }),
|
||||
onSelectProfile: id => postMessage({ type: 'selectAskProfile', id }),
|
||||
onSelectProfile: id => postMessage({ type: 'selectProfile', kind: 'ask', id }),
|
||||
}
|
||||
}, [state])
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { RewriteTree } from './BlockTree'
|
||||
import type { ChatState } from './qa'
|
||||
import type { ProfileChoice } from './types'
|
||||
import { useEffect, useMemo, useReducer, useRef } from 'react'
|
||||
import { chatReducer, EMPTY_CHAT, newAskId, unfinished } from './qa'
|
||||
import { toAskImage } from './qaImages'
|
||||
@@ -9,9 +8,9 @@ import { onMessage, postMessage } from './vscode'
|
||||
|
||||
/**
|
||||
* 按注释规范重写:收扩展端的重写记录、当前一轮的进展,给块树用的状态和动作。和问 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)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -29,6 +28,10 @@ export function useRewrite(profiles: ProfileChoice, onSelectProfile: (id: string
|
||||
case 'qa':
|
||||
dispatch({ type: 'sync', available: msg.available, threads: msg.rewrites, current: msg.rewriteCurrent })
|
||||
return
|
||||
case 'profiles':
|
||||
if (msg.kind === 'rewrite')
|
||||
dispatch({ type: 'profiles', profiles: { options: msg.options, selected: msg.selected } })
|
||||
return
|
||||
case 'rewriteProgress':
|
||||
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)
|
||||
@@ -51,8 +54,7 @@ export function useRewrite(profiles: ProfileChoice, onSelectProfile: (id: string
|
||||
|
||||
return useMemo((): RewriteTree => ({
|
||||
state,
|
||||
profiles,
|
||||
onSelectProfile,
|
||||
onSelectProfile: id => postMessage({ type: 'selectProfile', kind: 'rewrite', id }),
|
||||
onRewrite: (anchor) => {
|
||||
if (current.current.asks.some(ask => ask.anchor === anchor && unfinished(ask)))
|
||||
return
|
||||
@@ -73,5 +75,5 @@ export function useRewrite(profiles: ProfileChoice, onSelectProfile: (id: string
|
||||
dispatch({ type: 'close', anchor })
|
||||
postMessage({ type: 'forgetRound', kind: 'rewrite', anchor })
|
||||
},
|
||||
}), [state, profiles, onSelectProfile])
|
||||
}), [state])
|
||||
}
|
||||
Reference in new issue
Block a user