Claude Code VSCode 插件历史记录不显示问题修复记录

问题现象

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()

主要改动包括:

  1. 给 session 索引添加 fallback 路径;
  2. 兼容 Windows 路径大小写和项目目录映射;
  3. 避免 listSessions() 阶段整文件读取所有 .jsonl;
  4. 只在历史列表阶段读取 .jsonl 文件头尾内容,用于提取标题;
  5. 点击具体会话时,再由 getSession() 完整读取对应 session;
  6. 过滤 <ide_opened_file>、<ide_selected_code> 等 IDE 噪音事件,避免它们被当成会话标题或聊天消息显示;
  7. 对 zd()、Ld()、getSessionDiffs() 等可能卡住的异步调用增加 timeout,防止插件一直停留在 Loading sessions。

操作步骤

  1. 先找到 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
  1. 备份原文件:
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
  1. 打开extension.js,搜索并替换:async listSessions()async getSession(K),具体替换代码如下所示。
    替换完成后,在 VSCode 中执行:
bash 复制代码
Ctrl + Shift + P
Developer: Reload Window
  1. 如果插件异常,恢复备份:
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};
}
相关推荐
郝亚军2 小时前
win11安装python3.12.7和pycharm
ide·python·pycharm
资深流水灯工程师2 小时前
PyCharm 虚拟环境完整配置指南(PySide6 开发专用)
ide·python·pycharm
无足鸟ICT2 小时前
【RHCA+】移动光标快捷键
linux·编辑器·vim
sinat_2554878115 小时前
第七部分。介绍MVC(模型-视图-控制器)模式
java·ide·http·tomcat·intellij-idea
初一初十16 小时前
vue3茶叶商城网站vue网页vuejs前端
前端·javascript·vue.js·vscode·前端框架
Algorithm_Engineer_18 小时前
如何利用Pycharm进行分布式的Debug训练
ide·分布式·pycharm
Jumbo星19 小时前
新版vscode侧边资源管理器的文件搜索
ide·vscode·编辑器
今天的你比昨天进步了?19 小时前
单片机程序,keil可以正常编译,VScode编译报错处理
vscode·单片机·嵌入式硬件
ABAP-張旺19 小时前
ABAP:Visual Studio Code開發ABAP教程
ide·vscode·编辑器