/** * 「了解一块代码」找过的功能:问的话、栏标题、占哪个栏位、AI 最近一次交回的结果,按工作区存着。 * 这里是记录怎么读、怎么改,栏里一行、栏上方一句话怎么写。不碰 VS Code。 */ import type { CallNode, FeatureResult } from './featureResult' import { escapeMarkdown } from '../explorer/rowDecoration' import { freeSlot, SLOTS } from '../slots/slots' import { itemLabel, parseFeatureResult } from './featureResult' /** * 一个功能:question 是用户问的那句话(对话页标签上的名字,开新会话时发给 AI),name 是栏标题(开始时就是问题),result 是 AI 最近一次交回的结果(还没交回过时没有)。 * `outdated` 是存着的结果是以前的格式、读不了,只好当没有结果,栏上方提示重新找一次;交回新结果后就没有了。 */ export interface Feature { id: string question: string name: string slot: number result?: FeatureResult outdated?: true } /** 功能栏里找得到的节点行的 contextValue 带的标记(见 entryContext):行菜单只挂文件能用的那几样(问 AI、复制、在系统文件管理器中显示)。 */ export const FEATURE_MARKER = 'feature' /** 找不到的节点行的 contextValue:什么菜单都不挂。 */ export const MISSING_CONTEXT = `${FEATURE_MARKER} missing` /** 第 slot 个功能栏标题栏上的按钮命令:openChat 打开对话、rename 重命名、remove 删除。标题栏按钮不知道点的是哪一栏,所以每个栏位各一条。 */ export function featureCommand(action: 'openChat' | 'rename' | 'remove', slot: number): string { const name = { openChat: 'openFeatureChat', rename: 'renameFeature', remove: 'removeFeature' }[action] return `superpowers.codeReader.${name}${slot}` } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value) } /** 读 workspaceState 里存的功能;认不出的丢掉,同一个栏位存了两个时只认第一个,结果读不懂的当还没有结果。 */ export function parseFeatures(value: unknown): Feature[] { if (!Array.isArray(value)) return [] const features: Feature[] = [] for (const raw of value) { if (!isRecord(raw)) continue const { id, question, name, slot, result } = raw if (typeof id !== 'string' || typeof question !== 'string' || typeof name !== 'string' || typeof slot !== 'number' || !SLOTS.includes(slot)) continue if (features.some(feature => feature.slot === slot)) continue const parsed = result === undefined ? undefined : parseFeatureResult(result) const outdated = raw.outdated === true || (parsed !== undefined && 'error' in parsed) features.push({ id, question, name, slot, ...(parsed && 'result' in parsed && { result: parsed.result }), ...(outdated && { outdated }) }) } return features } /** 新功能占哪个栏位:和收藏共用,taken 是两者眼下占着的全部栏位;满了没有。 */ export function placeFeature(taken: Iterable): number | undefined { return freeSlot(taken) } /** 第 slot 栏的功能改叫 name,栏位不动。 */ export function renameFeature(features: readonly Feature[], slot: number, name: string): Feature[] { return features.map(feature => feature.slot === slot ? { ...feature, name } : feature) } /** id 那个功能换成这次交回的结果。 */ export function withResult(features: readonly Feature[], id: string, result: FeatureResult): Feature[] { return features.map(({ outdated, ...feature }) => feature.id === id ? { ...feature, result } : { ...feature, ...(outdated && { outdated }) }) } /** 删掉第 slot 栏的功能,空出这个栏位。 */ export function removeFeature(features: readonly Feature[], slot: number): Feature[] { return features.filter(feature => feature.slot !== slot) } /** * 栏里调用树上一个节点的那一行怎么画:名字(符号名,只有文件的写文件名)、后面淡色的说明(跨进程连过来的先写怎么连的,再写那一句话)、 * 悬停提示(文件路径和那一句话)。found 是按眼下的代码找没找到;找不到的悬停先说「找不到了」。 */ export function nodeRow(node: CallNode, found: boolean): { label: string, description: string, tooltip: string } { const lines = [`**${escapeMarkdown(node.path)}**`, escapeMarkdown(node.role)] return { label: itemLabel(node), description: node.via === undefined ? node.role : `${node.via} · ${node.role}`, tooltip: [...(found ? [] : ['找不到了']), ...lines].join(' \n'), } } /** 一个功能眼下在不在找:running 还在找(activity 是正在做的事),failed 没找成(error 是原因),stopped 被叫停了;没在找时没有。 */ export type FeatureRun = | { status: 'running', activity?: string } | { status: 'failed', error: string } | { status: 'stopped' } /** 句子没有句末标点时补个句号,后面好接下一句。 */ function sentence(text: string): string { return /[。!?.!?]$/.test(text) ? text : `${text}。` } /** * 栏上方的一句话(TreeView.message):正在找、没找成(可以在对话里接着说重试)、没找到相关的代码、还没有结果、存着的结果是旧格式(`outdated`,见 [Feature]); * 有结果、又没在找时不说话。有旧结果时再找没成,旧结果留着。 */ export function featureMessage(result: FeatureResult | undefined, run: FeatureRun | undefined, outdated = false): string | undefined { if (run?.status === 'running') { const doing = result ? '正在按你说的改…' : '正在找…' return run.activity ? `${doing}(${run.activity})` : doing } if (run?.status === 'failed') { return result ? `这次没改成:${sentence(run.error)}下面还是上一次的结果,可以在对话里接着说,让它再改一次。` : `没找成:${sentence(run.error)}可以在对话里接着说,让它再找一次。` } if (!result) { return outdated ? '结果是旧格式,点标题栏的「打开对话」,说一声「重新找」,就按路径和调用树重新找一次。' : '还没有结果,可以在对话里接着说,让它再找一次。' } if (result.paths.length === 0) return '没找到和这个功能直接相关的代码,可以在对话里换个说法接着说。' return undefined }