问题现象
CCSwitch + VsCode的Cluade code 插件
在 Windows 10 + VSCode 中使用 Claude Code 插件(Claude code版本是 2.1.68)时,关闭 VSCode 后重新打开,Past Conversations / History Sessions 页面无法正常显示历史会话,表现为:
- CLI 中 claude --resume 可以正常看到历史;
- claude/projects/.../*.jsonl 历史文件实际存在;
- VSCode 插件历史面板为空,或一直停留在 Loading sessions;
- 打过 GitHub issue #12872 中的补丁后,历史能被扫描到,但可能因为 .jsonl 文件过大或标题提取逻辑不完善,导致加载卡住或标题显示为 <ide_opened_file> 之类的 IDE 事件。
判断结论
该问题不是 Claude Code 配置丢失,也不是历史文件丢失,而是 VSCode 插件在 Windows 路径映射、session 索引和 .jsonl 读取逻辑上的兼容问题。
当前状态可理解为:
- Claude Code CLI:正常
- settings.json:正常
- 历史 .jsonl 文件:正常
- VSCode 插件历史面板:索引/渲染异常
修复思路
在 VSCode 插件的 extension.js 中修改两个核心方法:listSessions() 和 getSession()
主要改动包括:
- 给 session 索引添加 fallback 路径;
- 兼容 Windows 路径大小写和项目目录映射;
- 避免 listSessions() 阶段整文件读取所有 .jsonl;
- 只在历史列表阶段读取 .jsonl 文件头尾内容,用于提取标题;
- 点击具体会话时,再由 getSession() 完整读取对应 session;
- 过滤 <ide_opened_file>、<ide_selected_code> 等 IDE 噪音事件,避免它们被当成会话标题或聊天消息显示;
- 对 zd()、Ld()、getSessionDiffs() 等可能卡住的异步调用增加 timeout,防止插件一直停留在 Loading sessions。
操作步骤
- 先找到 Claude Code 插件目录(一般在
C:\Users\Administrator\.vscode\extensions\anthropic.claude-code-2.1.169-win32-x64),例如:
bash
Get-ChildItem "$env:USERPROFILE\.vscode\extensions" -Directory | Where-Object {
$_.Name -like "anthropic.claude-code-*"
} | Select-Object FullName
- 备份原文件:
bash
$ext = "C:\Users\Administrator\.vscode\extensions\anthropic.claude-code-2.1.168-win32-x64"
Copy-Item "$ext\extension.js" "$ext\extension.js.bak_loading" -Force
- 打开
extension.js,搜索并替换:async listSessions()和async getSession(K),具体替换代码如下所示。
替换完成后,在 VSCode 中执行:
bash
Ctrl + Shift + P
Developer: Reload Window
- 如果插件异常,恢复备份:
bash
Copy-Item "$ext\extension.js.bak_loading" "$ext\extension.js" -Force
最终建议
如果补丁生效,建议把最终可用版本单独备份:
bash
Copy-Item "$ext\extension.js" "$ext\extension.final_working.js" -Force
也可以备份到其他位置,避免 Claude Code 插件更新后覆盖:
bash
Copy-Item "$ext\extension.js" "D:\backup\claude\extension.final_working.js" -Force
后续如果插件更新后历史记录再次失效,可以重新对新版 extension.js 应用同样的 listSessions() 和 getSession() 修复逻辑。
总体结论:终端入口最稳定,VSCode 插件历史面板问题可以通过修改 extension.js 解决,但该补丁属于本地热修复,插件更新后可能需要重新应用。
javascript
async listSessions(){
let K=this.cwd;
const timeout=(p,ms,fallback)=>Promise.race([
p,
new Promise((resolve)=>setTimeout(()=>resolve(fallback),ms))
]);
const safeProjectKeys=(cwd)=>{
let arr=[];
try{
if(typeof ny==="function") arr.push(ny(cwd));
}catch{}
try{
if(typeof cwd==="string"){
arr.push(cwd.replace(/[\\/:]/g,"-"));
arr.push(cwd.replace(/[^a-zA-Z0-9]/g,"-"));
arr.push(cwd.replace(/\\/g,"-").replace(/\//g,"-").replace(/:/g,"-"));
if(/^[a-z]:\\/.test(cwd)){
let up=cwd[0].toUpperCase()+cwd.slice(1);
arr.push(up.replace(/[\\/:]/g,"-"));
arr.push(up.replace(/[^a-zA-Z0-9]/g,"-"));
}
}
}catch{}
return [...new Set(arr.filter(Boolean))];
};
const readSampleLines=(fs,file)=>{
try{
let st=fs.statSync(file);
let max=65536;
let fd=fs.openSync(file,"r");
try{
let headSize=Math.min(max,st.size);
let head=Buffer.alloc(headSize);
fs.readSync(fd,head,0,headSize,0);
let tail="";
if(st.size>max){
let tailSize=Math.min(max,st.size);
let tb=Buffer.alloc(tailSize);
fs.readSync(fd,tb,0,tailSize,Math.max(0,st.size-tailSize));
tail=tb.toString("utf8");
}
let text=head.toString("utf8")+"\n"+tail;
return text.split(/\r?\n/).filter((x)=>x.trim().length>0);
}finally{
fs.closeSync(fd);
}
}catch{
return [];
}
};
const isNoiseText=(s)=>{
if(!s||typeof s!=="string")return true;
let t=s.trim();
if(!t)return true;
return (
t.startsWith("<ide_opened_file>") ||
t.startsWith("<ide_selected_code>") ||
t.startsWith("<ide_diagnostics>") ||
t.startsWith("<local-command-stdout>") ||
t.startsWith("<local-command-stderr>") ||
t.includes("The user opened the file") ||
t.includes("The user selected") ||
t.includes("IDE context")
);
};
const extractSummary=(lines,id)=>{
let fallback=null;
for(let P of lines){
let T;
try{T=JSON.parse(P)}catch{continue}
// 1. 最高优先级:手动自定义标题
// 例如:
// {"type":"custom-title","customTitle":"xxx","sessionId":"ab48..."}
if(
T?.type==="custom-title" &&
typeof T.customTitle==="string" &&
T.customTitle.trim()
){
let title=T.customTitle.trim();
if(!isNoiseText(title)) return title;
}
// 2. 其次:Claude 自动标题
if(
T?.type==="ai-title" &&
typeof T.aiTitle==="string" &&
T.aiTitle.trim()
){
let title=T.aiTitle.trim();
if(!isNoiseText(title)) return title;
}
// 3. 再其次:summary
if(
!fallback &&
T?.type==="summary" &&
typeof T.summary==="string" &&
T.summary.trim()
){
let s=T.summary.trim();
if(!isNoiseText(s)) fallback=s;
}
// 4. 最后:第一条真实 user 输入
if(!fallback&&T?.type==="user"){
let h=T?.message?.content;
if(typeof h==="string"&&h.trim()){
let s=h.trim();
if(!isNoiseText(s)) fallback=s;
}else if(Array.isArray(h)){
let C=h.find((c)=>
c?.type==="text" &&
typeof c.text==="string" &&
c.text.trim() &&
!isNoiseText(c.text)
);
if(C) fallback=C.text.trim();
}
}
}
if(fallback&&fallback.length>70) fallback=fallback.slice(0,67)+"...";
return fallback||id;
};
let V=[];
try{
V=await timeout(zd({dir:K,includeWorktrees:!1}),2500,[]);
}catch(e){
this.logger.warn(`listSessions zd failed: ${e}`);
V=[];
}
if(V.length===0&&typeof K==="string"&&/^[a-z]:\\/.test(K)){
let q=K[0].toUpperCase()+K.slice(1);
if(q!==K){
try{
let z=await timeout(zd({dir:q,includeWorktrees:!1}),1500,[]);
if(z.length>0){
this.logger.warn(`listSessions path-case fallback: ${K} -> ${q} (${z.length} sessions)`);
K=q;
V=z;
}
}catch{}
}
}
let B=new Map();
try{
B=await timeout(g1.readTeleportMetadata(K,V.map((H)=>H.sessionId)),1500,new Map());
}catch(e){
this.logger.warn(`listSessions readTeleportMetadata failed: ${e}`);
B=new Map();
}
let j=V.map((H)=>{
let N=B.get(H.sessionId);
let wt;
try{wt=UR4(H.cwd)}catch{wt=H.cwd}
return {
id:H.sessionId,
lastModified:H.lastModified,
fileSize:H.fileSize,
summary:H.summary,
gitBranch:H.gitBranch,
worktree:wt,
isCurrentWorkspace:!0,
...N
};
});
if(j.length===0){
try{
let os=require("os"),path=require("path"),fs=require("fs");
let root=path.join(os.homedir(),".claude","projects");
let keys=safeProjectKeys(K);
let picked=null, files=[];
for(let key of keys){
let dir=path.join(root,key);
if(fs.existsSync(dir)){
let list=fs.readdirSync(dir).filter((F)=>F.endsWith(".jsonl"));
if(list.length>0){
picked=dir;
files=list;
break;
}
}
}
if(!picked&&fs.existsSync(root)){
let all=fs.readdirSync(root,{withFileTypes:!0}).filter((F)=>F.isDirectory()).map((F)=>F.name);
this.logger.warn(`listSessions fallback no direct project dir. cwd=${K} keys=${keys.join(",")} knownProjectDirs=${all.slice(0,30).join(",")}`);
}
if(picked){
let N=files.map((F)=>{
let M=path.join(picked,F);
let A=fs.statSync(M);
let id=F.replace(/\.jsonl$/,"");
let I=B.get(id);
let lines=readSampleLines(fs,M);
try{
let all=fs.readFileSync(M,"utf8").split(/\r?\n/).filter((x)=>x.includes('"custom-title"')||x.includes('"ai-title"'));
lines=all.concat(lines);
}catch{}
let R=I?.summary||extractSummary(lines,id);
return {
id,
lastModified:A.mtimeMs,
fileSize:A.size,
summary:R||id,
gitBranch:I?.gitBranch,
worktree:I?.worktree||K,
isCurrentWorkspace:!0,
...I
};
}).sort((a,b)=>b.lastModified-a.lastModified);
if(N.length>0){
this.logger.warn(`listSessions file fallback used: ${N.length} sessions from ${picked}`);
j=N;
}
}
}catch(q){
this.logger.warn(`listSessions debug failed: ${q}`);
}
}
try{ja(j)}catch(e){this.logger.warn(`listSessions ja validation failed: ${e}`)}
let Y=new Set(this.settings.getHiddenSessionIds());
let X=Y.size>0?j.filter((H)=>!Y.has(H.id)):j;
if(X.length===0&&j.length>0){
this.logger.warn(`listSessions fallback: hidden filter removed all sessions (cwd=${K}, total=${j.length}, hidden=${Y.size})`);
X=j;
}
this.logger.log(`listSessions cwd=${K} total=${j.length} hidden=${Y.size} returned=${X.length}`);
return {type:"list_sessions_response",sessions:X};
}
javascript
async getSession(K){
let V=this.cwd,B;
const timeout=(p,ms,fallback)=>Promise.race([
p,
new Promise((resolve)=>setTimeout(()=>resolve(fallback),ms))
]);
const safeProjectKeys=(cwd)=>{
let arr=[];
try{
if(typeof ny==="function") arr.push(ny(cwd));
}catch{}
try{
if(typeof cwd==="string"){
arr.push(cwd.replace(/[\\/:]/g,"-"));
arr.push(cwd.replace(/[^a-zA-Z0-9]/g,"-"));
arr.push(cwd.replace(/\\/g,"-").replace(/\//g,"-").replace(/:/g,"-"));
if(/^[a-z]:\\/.test(cwd)){
let up=cwd[0].toUpperCase()+cwd.slice(1);
arr.push(up.replace(/[\\/:]/g,"-"));
arr.push(up.replace(/[^a-zA-Z0-9]/g,"-"));
}
}
}catch{}
return [...new Set(arr.filter(Boolean))];
};
const loadFromFile=(cwd,sessionId)=>{
try{
let os=require("os"),path=require("path"),fs=require("fs");
let root=path.join(os.homedir(),".claude","projects");
for(let key of safeProjectKeys(cwd)){
let q=path.join(root,key,`${sessionId}.jsonl`);
if(fs.existsSync(q)){
let z=fs.readFileSync(q,"utf8").split(/\r?\n/).filter((U)=>U.trim().length>0);
let out=[];
for(let U of z){
try{out.push(JSON.parse(U))}catch{}
}
this.logger.warn(`getSession file fallback used: session=${sessionId} file=${q} raw=${out.length}`);
return out;
}
}
}catch(e){
this.logger.warn(`getSession file fallback failed: ${e}`);
}
return null;
};
try{
B=await timeout(Ld(K,{dir:V}),2500,null);
}catch(j){
if(typeof V==="string"&&/^[a-z]:\\/.test(V)){
let Y=V[0].toUpperCase()+V.slice(1);
if(Y!==V){
try{
B=await timeout(Ld(K,{dir:Y}),1500,null);
if(B){
this.logger.warn(`getSession path-case fallback: ${V} -> ${Y} (session=${K})`);
V=Y;
}
}catch{}
}
}
if(!B){
B=loadFromFile(V,K);
}
if(!B) throw j;
}
if(!B||B.length===0){
let F=loadFromFile(V,K);
if(F&&F.length>0){
B=F;
this.logger.warn(`getSession empty->file fallback used: session=${K} raw=${B.length}`);
}
}
let j;
try{
j=await timeout(
(async()=>await(await g1.load(V,this.logger)).getSessionDiffs(K,V,B||[]))(),
2000,
void 0
);
}catch(e){
this.logger.warn(`getSession diffs skipped: ${e}`);
j=void 0;
}
const isNoiseMessage=(H)=>{
try{
if(!H||H.type!=="user")return false;
let h=H?.message?.content;
let texts=[];
if(typeof h==="string") texts.push(h);
else if(Array.isArray(h)){
for(let x of h){
if(x?.type==="text"&&typeof x.text==="string") texts.push(x.text);
}
}
return texts.some((s)=>{
let t=s.trim();
return (
t.startsWith("<ide_opened_file>") ||
t.startsWith("<ide_selected_code>") ||
t.startsWith("<ide_diagnostics>") ||
t.includes("The user opened the file")
);
});
}catch{
return false;
}
};
let Y=(B||[]).filter((H)=>
!isNoiseMessage(H)&&(
H?.type==="user"||
H?.type==="assistant"||
H?.type==="system"||
H?.type==="meta"||
H?.type==="compact"
)
);
this.logger.log(`getSession id=${K} raw=${(B||[]).length} filtered=${Y.length} cwd=${V}`);
return {type:"get_session_response",messages:Y,sessionDiffs:j};
}