✨ feat(vscode): 文件和文件夹在预览顶部问

侧栏「问 AI」打开代码阅读预览。文件夹是没有代码块的一页,源码文件在整页顶部聊,代码块下面的问答不动。历史按工作区文件夹和相对路径记在窗口状态里,不写进按块存的记录。
This commit is contained in:
cruldra committed 2026-10-03 03:22:20 +08:00
1 parent 2be635d1e5
commit 2c10acf9c4
19 files changed
+610 -459

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
/**
* 预览顶部的问答:按「工作区文件夹 + 相对路径」记在这个窗口的 workspaceState 里。
* 文件和文件夹各用各的路径;不写进按块存的 `.spx/code-qa.json`。
*/
import type { QaEntry } from './qaMap'
import * as path from 'node:path'
import { normalizeKey } from '../pathMap'
/** workspaceState 里这份表的键 */
export const PATH_QA_STATE_KEY = 'codeReader.pathQa'
/**
* 顶部这一轮的记号。块的记号是「种类 名字」,不会是这个,
* 所以页面上按记号取问答时,顶部的不会挂到某一块下面。
*/
export const PATH_ASK_ANCHOR = '\0path'
/** 工作区文件夹 Uri → 那条路径上的问答(按问的先后) */
export type PathQaMap = Record<string, QaEntry[]>
/** 工作区根的相对路径是空字符串,跟任何一个文件或子目录都不同。 */
export function relativeQaKey(rootPath: string, targetPath: string): string | undefined {
const relative = path.posix.relative(rootPath, targetPath)
if (relative === '')
return ''
return normalizeKey(relative)
}
/** 同一工作区文件夹、同一相对路径才是同一条记录。 */
export function pathQaKey(folderUri: string, relativePath: string): string {
return `${folderUri}\n${relativePath}`
}
/** 给人看的相对路径;工作区根写成 `.`。 */
export function displayRelativePath(relativePath: string): string {
return relativePath === '' ? '.' : relativePath
}
export function pathQaEntries(map: PathQaMap, key: string): QaEntry[] {
return map[key] ?? []
}
/** 新的一次问答接在这条路径已有的后面,别的路径不动。 */
export function withPathQa(map: PathQaMap, key: string, entry: QaEntry): PathQaMap {
return { ...map, [key]: [...pathQaEntries(map, key), entry] }
}
/** 删掉这一次;这条路径问光了就把键拿掉。没有这一次时原样返回。 */
export function withoutPathQa(map: PathQaMap, key: string, entryId: string): PathQaMap {
const entries = pathQaEntries(map, key)
const kept = entries.filter(entry => entry.id !== entryId)
if (kept.length === entries.length)
return map
if (kept.length === 0) {
const next = { ...map }
delete next[key]
return next
}
return { ...map, [key]: kept }
}
/** 读 workspaceState 里存的表;认不出的丢掉,不让一条坏数据把整份弄丢。 */
export function parsePathQa(value: unknown): PathQaMap {
if (!isRecord(value))
return {}
const map: PathQaMap = {}
for (const [key, entries] of Object.entries(value)) {
if (!Array.isArray(entries))
continue
const parsed = entries.flatMap((entry) => {
const one = parseEntry(entry)
return one ? [one] : []
})
if (parsed.length > 0)
map[key] = parsed
}
return map
}
function parseEntry(value: unknown): QaEntry | undefined {
if (!isRecord(value))
return undefined
const { id, question, answer, createdAt, sessionId, profile, images } = value
if (typeof id !== 'string' || typeof question !== 'string' || typeof answer !== 'string' || typeof createdAt !== 'string' || typeof sessionId !== 'string')
return undefined
const entry: QaEntry = { id, question, answer, createdAt, sessionId }
if (typeof profile === 'string' && profile !== '')
entry.profile = profile
if (Array.isArray(images) && images.every(name => typeof name === 'string'))
entry.images = images
return entry
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}