Lesson 0003 StatusBarItem:状态栏 UI 原语

前两课我们学习了扩展怎么说package.json 声明)和怎么活activate/deactivate 生命周期)。这一课我们进入扩展真正做的事——在 VS Code 状态栏上显示信息。StatusBarItem 是 claude-context-bar 使用的唯一 UI 原语,约 300 行代码围绕它展开。

1. StatusBarItem 是什么?

VS Code 窗口底部有一条状态栏(Status Bar)——就是显示行号、编码、语言模式的那条蓝底横条。扩展可以向其中添加自定义条目,每个条目就是一个 StatusBarItem

claude-context-bar 在状态栏中为每个活跃的 Claude Code 会话创建一个条目,效果类似:

🖼️ 想象这个场景:你同时打开了 3 个 Claude Code 会话——webapp(45%)、api-server(72%)、docs-site(12%)。状态栏右侧会出现三个条目:

🌐 webapp: 45% ⚙️ api-server: 72% 📚 docs-site: 12%

点击任意条目可以隐藏它,悬停可以看到详细的 token 用量分解。这些条目在会话文件变化时实时更新
🤔 为什么偏偏是状态栏?VS Code 给了扩展很多显示位置——侧边栏 TreeView、独立标签页 WebviewPanel、悬停卡片 HoverProvider、右上角通知……但 Context 用量是一个状态指标(类似行号、编码),需要始终可见但不抢占注意力。状态栏是这个场景的天然匹配。侧边栏 TreeView 可以展示更丰富的结构(每个会话展开看 token 分解),代价是需要用户主动打开侧边栏。

本课程聚焦于 claude-context-bar 的实际选择。其他显示位置的概念会在后续课程中作为扩展视野介绍。

2. 创建 StatusBarItem

VS Code 提供两个创建方法,用于不同的对齐方向

const item = vscode.window.createStatusBarItem(
    vscode.StatusBarAlignment.Right,  // 对齐方向
    100                               // 优先级
);

2.1 对齐方向(Alignment)

枚举值位置典型用途
StatusBarAlignment.Left状态栏左侧应用级信息(Git 分支、错误计数)
StatusBarAlignment.Right状态栏右侧编辑器状态(行号、编码、语言模式)

claude-context-bar 使用 Right 对齐——这与 VS Code 原生的行号、编码等信息放在同一区域,视觉上自然融入编辑器。

🔍 为什么不是 Left?左侧通常留给"当前在做什么"的信息(Git 分支、任务状态)。右侧留给"当前状态是什么"的信息(光标位置、文件编码、token 用量)。claude-context-bar 展示的是 Claude Code 会话的状态指标,属于后者。

2.2 优先级(Priority)—— 最反直觉的概念

优先级决定了条目在状态栏中的排列顺序,但它的行为取决于对齐方向:

对齐方向高优先级 = 更靠近……类比
Left左侧(先到先得)从左往右排队,大号排前面
Right左侧(逻辑上更靠前)从右往左排队,大号更靠左

对于 Right 对齐:优先级越高,条目越靠左。这不是 bug——VS Code 用"高优先级 = 更少被挤出"的语义来设计排序。

想象状态栏右侧空间有限。当窗口缩小时,低优先级的条目先被隐藏。高优先级的条目更"稳固",所以放在更靠左(更安全)的位置。

claude-context-bar 中的代码:

// refreshAllSessions() 中的关键行(extension.ts 第 664 行)
const priority = 900 + (sessions.length - i);
// 900 是基础值——确保它在 VS Code 原生条目(~100-800)的左边
// +(sessions.length - i) 让多个会话从老到新递增排列
// 最高的优先级给最老的会话,它最靠左
📐 优先级数值经验法则:VS Code 原生条目通常占据 0–800 的范围。如果你想确保自己的条目比原生条目更靠左,使用 900+ 的优先级。claude-context-bar 用 900 就是这个原因。
🎯 一句话记住优先级:

不管是 Left 还是 Right 对齐,优先级数字越大,条目越靠左。别管方向——记住"大数字 = 靠左"就够了。

原因是 Left 区和 Right 区的渲染方向不同,但结果一样:Left 区从左往右填(大号先占位),Right 区"大号更稳固所以放安全位置(靠左)"。视觉效果是一致的。

2.3 状态栏渲染示意图

状态栏全长:
┌─────────────────────────────────────────────────────────────┐
│ [Left 区]               [弹性空白]            [Right 区]     │
│                                                             │
│ Priority:  1000 ──→ 500                1000 ──→ 500         │
│ Preview:   [A][B]    [C]     ...       [X][Y]    [Z]       │
│            左 ──→ 右                     左 ──→ 右           │
│                                                             │
│ 渲染方向:   → 从左往右填                  ← 从右往左填        │
│ 大数字:     先抢到左边的好位置              更稳固,放在左边    │
└─────────────────────────────────────────────────────────────┘

结论:不管你用 Left 还是 Right,数字大的永远在左边。
区别只在于 Left 区从窗口左边缘开始排队,Right 区从窗口右边缘开始排队。

3. StatusBarItem 的属性与方法

3.1 核心属性

属性类型说明
textstring条目上显示的文本(支持 emoji)
tooltipstring | MarkdownString悬停提示——Markdown 格式可获得丰富排版
commandstring | Command点击时触发的命令。可以是简单字符串(命令 ID)或对象(带参数)
colorstring | ThemeColor文字颜色
backgroundColorThemeColor背景颜色——VS Code 提供语义化颜色常量
alignmentStatusBarAlignment对齐方向(创建时设定,只读)
prioritynumber优先级(创建时设定,只读)

3.2 方法

方法说明
show()在状态栏中显示此条目
hide()隐藏此条目(但不销毁——可以再次 show)
dispose()销毁此条目,释放资源。之后不能再使用
⚠️ show/hide vs dispose:hide() 只是从状态栏中移除显示,对象还活着。但 dispose() 是彻底销毁——之后任何操作都可能导致错误。claude-context-bar 在清理过时会话时调用 dispose(),在隐藏被用户手动关闭的会话时只是从 Map 中移除。

4. 完整生命周期:从创建到销毁

打开 extension.ts,追踪 refreshAllSessions()(第 590 行起)中 StatusBarItem 的完整生命周期:

阶段一:创建(第 662–670 行)

if (!entry) {
    // 这个会话还没有状态栏条目——创建一个新的
    const priority = 900 + (sessions.length - i);
    const item = vscode.window.createStatusBarItem(
        vscode.StatusBarAlignment.Right,
        priority
    );
    entry = { item, sessionFile: session.sessionFile };
    statusBarItems.set(session.sessionFile, entry);  // 登记到全局 Map
}

新会话出现时创建条目并登记到全局 MapstatusBarItems)中。这个 Map 是扩展的"条目注册表"——它追踪每个会话文件对应哪个 StatusBarItem。

阶段二:配置(第 674–712 行)

// 1. 设置显示文本
const icon = showEmoji ? getEmojiForProject(session.projectName) : '';
entry.item.text = `${icon} ${displayName}: ${session.percentage}%`;

// 2. 根据 token 用量百分比设置背景色
if (session.percentage >= dangerThreshold) {
    entry.item.backgroundColor = new vscode.ThemeColor('statusBarItem.errorBackground');
} else if (session.percentage >= warningThreshold) {
    entry.item.backgroundColor = new vscode.ThemeColor('statusBarItem.warningBackground');
} else {
    entry.item.backgroundColor = undefined;  // 恢复默认
}

// 3. 设置文字颜色(项目专属色彩)
entry.item.color = projectColorMap.get(session.projectName) || '#ffffff';

// 4. 设置悬停提示(Markdown 格式)
entry.item.tooltip = new vscode.MarkdownString(
    `**${session.projectName}** ...\n\n` +
    `📊 **Context Usage: ${session.percentage}%**\n\n` +
    `...`
);

// 5. 设置点击行为
entry.item.command = {
    command: 'claudeContextBar.hideSession',
    title: 'Hide Session',
    arguments: [session.sessionFile]   // 传递参数给命令回调
};
🎯 关键模式:属性即 UI

StatusBarItem 的所有可视属性(textcolorbackgroundColortooltip)都是直接赋值,没有 update()render() 方法。你改了属性,VS Code 自动重绘。这是 VS Code Extension API 中贯穿始终的声明式 UI模式——你声明最终状态,VS Code 负责渲染。

阶段三:显示(第 714 行)

entry.item.show();  // 在状态栏中显示此条目

创建出来的 StatusBarItem 默认是不可见的。只有调用了 show() 之后它才出现在状态栏中。每次 refreshAllSessions() 都会对所有活跃会话调用 show()——但重复调用是无害的(对已显示的条目再次 show 没有副作用)。

阶段四:更新(每次 refreshAllSessions 被调用)

整个配置阶段(阶段二)在每次 refreshAllSessions() 执行时都会重新运行——无论是文件监听触发、定时器触发、还是配置变化触发。这意味着条目的 text、color、backgroundColor 和 tooltip 始终反映最新的 token 用量。

触发刷新的三条路径:

// 路径 1:会话文件变化(文件监听)
fileWatcher = fs.watch(claudeProjectsDir, { recursive: true },
    (event, filename) => {
        if (filename?.endsWith('.jsonl')) {
            refreshAllSessions();  // 立即刷新
        }
    }
);

// 路径 2:定时轮询(兜底)
refreshInterval = setInterval(refreshAllSessions, intervalSeconds * 1000);

// 路径 3:用户修改配置
vscode.workspace.onDidChangeConfiguration(e => {
    if (e.affectsConfiguration('claudeContextBar')) {
        refreshAllSessions();
    }
});

阶段五:销毁(第 718–723 行)

// 移除已经不再活跃的会话条目
for (const [sessionFile, entry] of statusBarItems) {
    if (!seenPaths.has(sessionFile)) {
        entry.item.dispose();            // 销毁 StatusBarItem
        statusBarItems.delete(sessionFile);  // 从注册表中移除
    }
}

当会话不再活跃(超过 idleTimeout 没有活动),它的 sessionFile 不会出现在本次扫描的 seenPaths 中。此时扩展销毁对应的 StatusBarItem 并从 Map 中移除——dispose 和 delete 必须配对,否则 Map 会积累已销毁对象的引用(内存泄漏)。

⚠️ 常见错误:dispose()delete。已 dispose 的 StatusBarItem 对象仍然占据 Map 的键,下次遍历时可能尝试访问它的属性——VS Code 会抛出异常提示"对象已 disposed"。反过来,只 deletedispose 会导致状态栏条目一直显示(僵尸条目)。

5. 数据模型:sessionFile 作为主键

状态栏 UI 的背后是一个sessionFile 为索引的数据模型。理解它是理解整个扩展数据流的关键。

5.1 sessionFile 是什么

sessionFile 就是 Claude Code 会话 JSONL 文件的磁盘完整路径

C:\Users\xiaos\.claude\projects\C--dev-webapp\a1b2c3d4.jsonl
                                        \e5f6g7h8.jsonl

一个 session 有 projectName(显示名,可能重复)、sessionId(文件名的前 8 位),但只有 sessionFile绝对唯一的——磁盘上不可能有两个同路径的文件。因此它充当了整个扩展的主键

// sessionFile 在代码中扮演的所有"索引"角色
statusBarItems.get(session.sessionFile)       // Map 的 key
hiddenSessions.set(sessionFile, Date.now())   // 隐藏列表的 key
seenPaths.add(session.sessionFile)            // Set 的成员
arguments: [session.sessionFile]              // 命令参数——点击时传递给 hideSession 回调
🔑 一句话:sessionFile 就是会话的"身份证号"。整个扩展中所有需要"找某个会话"的地方,都用它当索引。

5.2 全局注册表:statusBarItems Map

statusBarItems 是一个 Map<string, StatusBarEntry>——它是扩展的"条目注册表",在 refreshAllSessions() 调用之间持续存在(定义在模块顶层,不是函数内的局部变量)。

// 每次 refreshAllSessions() 中的查找逻辑
let entry = statusBarItems.get(session.sessionFile);  // 先查 Map:这个会话已经有条目了吗?
if (!entry) {
    // 没有——创建一个新条目,登记到 Map
    entry = { item, sessionFile: session.sessionFile };
    statusBarItems.set(session.sessionFile, entry);
}
// 有——复用现有条目,更新属性即可

这个设计保证了同一个会话在多次刷新之间复用同一个 StatusBarItem 对象——不会因为刷新而闪烁或重新排列。

5.3 清理机制:seenPaths 的标记-清除模式

seenPathsrefreshAllSessions() 内部的一个局部 Set,用于实现经典的标记-清除(mark-and-sweep)算法:

// refreshAllSessions() 中 seenPaths 的完整生命周期
// ═══ 阶段一:标记 ═══
const seenPaths = new Set<string>();         // 每次刷新从零开始

for (const session of sessions) {
    seenPaths.add(session.sessionFile);       // "这个会话我这次看到了"
    // ... 创建或更新 StatusBarItem ...
}

// ═══ 阶段二:清除 ═══
for (const [sessionFile, entry] of statusBarItems) {
    if (!seenPaths.has(sessionFile)) {      // "在 statusBarItems 中,但这次没看到"
        entry.item.dispose();                 // → 不活跃了,干掉
        statusBarItems.delete(sessionFile);
    }
}
// seenPaths 随函数返回销毁——不保留任何状态
数据结构作用域作用
statusBarItems(Map)模块全局长期记忆——"当前有哪些条目"
seenPaths(Set)refreshAllSessions() 局部一次性快照——"本次扫描看到了哪些会话"
hiddenSessions(Map)模块全局用户手动隐藏的会话及其时间戳

判断标准极其简单:一个会话不在此次 findActiveSessions() 的返回结果中 → 它的 sessionFile 不在 seenPaths 中 → 不活跃 → 清理。

6. ThemeColor:自适应主题的颜色系统

claude-context-bar 没有硬编码颜色值(如 #ff0000),而是使用了 VS Code 的 ThemeColor

new vscode.ThemeColor('statusBarItem.errorBackground')
new vscode.ThemeColor('statusBarItem.warningBackground')

ThemeColor 是一个语义化颜色引用——它不指定具体颜色值,而是表达"这个元素在当前主题中应该用什么颜色"。当用户切换主题(深色 ↔ 浅色)时,VS Code 自动解析为对应的实际颜色。

ThemeColor 键深色主题浅色主题
statusBarItem.errorBackground暗红色浅红色
statusBarItem.warningBackground暗黄色浅黄色
🆕 对你来说:如果你来自桌面/移动开发背景,ThemeColor 类似于 Android 的 ?attr/colorError 或 iOS 的 UIColor.systemRed——都是"让系统根据上下文决定具体颜色"的间接引用。永远不要在 VS Code 扩展中硬编码 UI 颜色,除非你确定用户永远用同一款主题。

7. refreshAllSessions() 的整体结构

现在你已经有足够的知识来理解 refreshAllSessions() 的完整结构。打开 extension.ts 第 590 行,它的逻辑框架如下:

  1. 收集数据:调用 findActiveSessions() 扫描文件系统,返回活跃会话列表
  2. 读取配置:从 vscode.workspace.getConfiguration() 读取所有用户设置
  3. 分配颜色:根据 autoColor 模式为每个项目分配颜色(调色板或基准色变化)
  4. 创建/更新条目:遍历会话列表,为每个会话创建或更新 StatusBarItem
  5. 清理过期条目:销毁不再活跃的会话对应的 StatusBarItem

这个五步循环在文件变化、定时器到期、配置修改时反复执行。StatusBarItem 是这个循环的"输出端"——所有数据收集和处理最终都投射到这几个状态栏条目上。

8. 同项目多会话的命名

如果你在同一个项目(如 C:\dev\webapp)中同时开了多个 Claude Code 会话,findActiveSessions() 会自动给它们分配稳定编号

同一个 projectName 的所有会话被放入一个 projectGroups Map

组内按创建时间排序,过滤掉被淘汰的(/clear 的、被新会话覆盖的)

按创建时间从老到新排序

最老的 → 保留原名  'webapp'
第二个 → 'webapp-2'
第三个 → 'webapp-3'
  ... 以此类推

编号是稳定的——排序依据是 sessionCreated 时间戳,最老的永远是原始名,后来的按时间顺序递增。不会因为某次刷新顺序变了就让 webapp-2 突然变成 webapp

8.1 compact 模式下的显示

当用户开启 compactMode 后,getShortName() 会把项目名缩写,但会话编号后缀会被保留

normal 模式:  webapp-2: 72%
compact 模式: W-2: 72%

// getShortName 的缩写逻辑(extension.ts 第 256 行):
// 多单词 → 首字母缩写:my-cool-project → MCP
// 单单词 → 首字母 + 尾音节:typescript → Tscript
// 短名(≤5 字符)→ 保持不变
// 会话编号(-2, -3)→ 始终保留

用户还可以在 settings.json 中通过 claudeContextBar.shortNames 给特定项目自定义显示名,完全覆盖自动缩写。

📌 下一课预告:findActiveSessions() 是这一切的数据来源——它负责扫描文件系统、解析 JSONL、过滤淘汰的会话、分配编号。Lesson 0004 将完整剖析它。

9. 练习:追踪一个条目的完整生命周期

  1. 打开 extension.ts,在第 662 行找到 createStatusBarItem 调用
  2. 从该行向下阅读,列出对 entry.item 的每一次属性赋值(text, color, backgroundColor, tooltip, command, show)
  3. 在第 718 行找到 dispose 循环——理解 seenPaths 是如何被用来判断"哪些会话该被清理"的
  4. 回到 activate() 函数(第 36 行),画出三条触发 refreshAllSessions() 的路径

10. 知识检查

问题 1

对于 Right 对齐的 StatusBarItem,优先级 1000 和优先级 100 哪个更靠左?

问题 2

vscode.ThemeColor('statusBarItem.errorBackground') 的作用是什么?

问题 3

claude-context-bar 的 refreshAllSessions() 末尾,为什么既要 dispose() 又要 delete

问题 4

创建 StatusBarItem 后,它默认是可见的还是隐藏的?

问题 5

对于 Left 对齐的 StatusBarItem,优先级 1000 和优先级 100 哪个更靠左?

问题 6

在 claude-context-bar 中,sessionFile 充当什么角色?

11. 推荐深入阅读

💡 提问提示:如果你对 StatusBarItem 的优先级规则、ThemeColor 的工作原理、或者 refreshAllSessions 中任何一段代码有疑问,直接在这个对话中提问。

📌 下一课(Lesson 0004)将深入 findActiveSessions()——文件系统扫描、JSONL 数据解析、会话淘汰逻辑、token 计数算法。那是本课第 7 节第一步"收集数据"的完整展开。