127 lines
6.4 KiB
TypeScript
127 lines
6.4 KiB
TypeScript
/**
|
||
* 「了解一块代码」找过的功能:问的话、栏标题、占哪个栏位、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<string, unknown> {
|
||
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>): 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
|
||
}
|