✨ feat(vscode): 会话管理支持导入已 /rename 的命名会话
This commit is contained in:
1 parent
7c67d220db
commit
dc522064ce
10 files changed
+438
-9
No files matched your search
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 从 Claude projects 目录扫描已 /rename(jsonl 含 custom-title)且尚未登记到
|
||||
* managed store 的会话,供「导入」候选列表使用。
|
||||
*/
|
||||
|
||||
import { promises as fsp } from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { projectsDirFor } from '../cc/sessionWatcher'
|
||||
import { readManagedSessions } from './managedStore'
|
||||
|
||||
export interface ImportableNamedSession {
|
||||
id: string
|
||||
name: string
|
||||
mtimeMs: number
|
||||
}
|
||||
|
||||
/** 从 jsonl 文本提取最后一条 custom-title(trim 后非空),否则 null */
|
||||
export function extractLastCustomTitle(jsonlText: string): string | null {
|
||||
let last: string | null = null
|
||||
const lines = jsonlText.split(/\r?\n/)
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed)
|
||||
continue
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(trimmed)
|
||||
}
|
||||
catch {
|
||||
continue
|
||||
}
|
||||
if (!parsed || typeof parsed !== 'object')
|
||||
continue
|
||||
const rec = parsed as { type?: unknown, customTitle?: unknown }
|
||||
if (rec.type !== 'custom-title')
|
||||
continue
|
||||
if (typeof rec.customTitle !== 'string')
|
||||
continue
|
||||
const title = rec.customTitle.trim()
|
||||
if (!title)
|
||||
continue
|
||||
last = title
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出可导入命名会话:扫 projectsDir 顶层 *.jsonl,过滤已在 managed 的 id,mtime 降序。
|
||||
* `projectsDir` 可选,便于测试注入;默认 `projectsDirFor(workspaceRoot)`。
|
||||
*/
|
||||
export async function listImportableNamedSessions(
|
||||
workspaceRoot: string,
|
||||
projectsDir?: string,
|
||||
): Promise<ImportableNamedSession[]> {
|
||||
const dir = projectsDir ?? projectsDirFor(workspaceRoot)
|
||||
let entries: import('node:fs').Dirent[]
|
||||
try {
|
||||
entries = await fsp.readdir(dir, { withFileTypes: true })
|
||||
}
|
||||
catch {
|
||||
return []
|
||||
}
|
||||
|
||||
const managed = await readManagedSessions(workspaceRoot)
|
||||
const managedIds = new Set(managed.sessions.map(s => s.id))
|
||||
|
||||
const results: ImportableNamedSession[] = []
|
||||
for (const ent of entries) {
|
||||
if (!ent.isFile() || !ent.name.endsWith('.jsonl'))
|
||||
continue
|
||||
const id = ent.name.slice(0, -'.jsonl'.length)
|
||||
if (!id || managedIds.has(id))
|
||||
continue
|
||||
const filePath = path.join(dir, ent.name)
|
||||
let text: string
|
||||
let mtimeMs: number
|
||||
try {
|
||||
const [raw, st] = await Promise.all([
|
||||
fsp.readFile(filePath, 'utf8'),
|
||||
fsp.stat(filePath),
|
||||
])
|
||||
text = raw
|
||||
mtimeMs = st.mtimeMs
|
||||
}
|
||||
catch {
|
||||
continue
|
||||
}
|
||||
const name = extractLastCustomTitle(text)
|
||||
if (!name)
|
||||
continue
|
||||
results.push({ id, name, mtimeMs })
|
||||
}
|
||||
|
||||
results.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
||||
return results
|
||||
}
|
||||
Reference in new issue
Block a user