Skip to content

feat(cli): 添加可插拔状态栏 (statusline)#193

Merged
qorzj merged 7 commits into
lessweb:mainfrom
dengmik-commits:feat/statusline
Jun 25, 2026
Merged

feat(cli): 添加可插拔状态栏 (statusline)#193
qorzj merged 7 commits into
lessweb:mainfrom
dengmik-commits:feat/statusline

Conversation

@dengmik-commits

Copy link
Copy Markdown
Contributor

feat(cli): 可插拔状态栏 (statusline)

概述

为 Deep Code CLI 增加 可插拔状态栏:在输入框下方渲染一行由用户配置的 segment(git 分支、模型信息、token/会话统计、工具调用次数等),通过 settings.jsonstatusline 字段声明,无需改动 CLI 源码即可扩展。

灵感来自 Claude Code 的 statusline 机制,但实现完全本地化、零外部依赖。

动机

CLI 长会话中常需要随时观察的上下文(当前模型/思考强度、cwd、git 分支、已用 token、context 占用百分比、工具调用分布……)目前散落在各处或需要中断会话去查。提供一个由用户自由组合的状态栏后:

  • 模型/思考模式切换一目了然
  • token 使用接近 compact 阈值时及早察觉
  • 工具调用热度统计帮助 review 会话行为
  • 任何外部信息(CI 状态、TODO 数、ts 错误数 ...)都可由用户写脚本接入

设计

配置入口

新增顶层字段 settings.json#statusline

{
  "statusline": {
    "enabled": true,
    "refreshMs": 2000,
    "separator": " · ",
    "providers": [
      { "type": "module", "id": "model",   "path": "./.deepcode/plugins/model-info.mjs",   "color": "white" },
      { "type": "module", "id": "cwd",     "path": "./.deepcode/plugins/cwd.mjs",          "color": "cyan" },
      { "type": "module", "id": "branch",  "path": "./.deepcode/plugins/git-branch.mjs",   "color": "magenta" },
      { "type": "module", "id": "session", "path": "./.deepcode/plugins/session-stats.mjs","color": "yellow" },
      { "type": "module", "id": "tools",   "path": "./.deepcode/plugins/tool-usage.mjs",   "color": "green" }
    ]
  }
}
  • 用户级 (~/.deepcode/settings.json) 与项目级 (<project>/.deepcode/settings.json) 的 providers 数组按 追加 方式合并(用户先、项目后),其他字段项目级优先。
  • 缺省 refreshMs = 2000,最小 500;缺省 separator = " · "

Provider 两种类型

类型 说明
command 在 shell 执行命令,取 stdout 第一行;用于现成 CLI(git/node/date 等)
module 动态 import() 一个本地 .mjs/.js,调用其默认导出函数,传入 { projectRoot, session }

SessionInfo(仅 module provider 可见):

{
  activeSessionId: string | null,
  messageCount: number,
  requestCount: number,
  totalTokens: number,
  activeTokens: number,
  maxContextTokens: number,   // 来自 getCompactPromptTokenThreshold(model)
  model: string,
  thinkingEnabled: boolean | undefined,
  reasoningEffort: string | undefined,
  toolUsage: Record<string, number>,
}

安全限制

  • module provider 的 path 解析后必须位于 项目根目录用户家目录 之下,绝对路径越界会被拒绝加载,防止任意位置执行代码。
  • 单个 segment 文本自动 sanitize:取首个非空行、剥离 ANSI 序列、折叠空白、超过 40 字符截断(加 )。
  • command provider stdout 上限 4 KB,超时(缺省 1500 ms)后该段返回空。
  • 任一 provider 抛错/超时/返回空 → 仅跳过该段,不影响其它 provider,也不影响 CLI 主流程。

刷新与渲染

  • 启动后立即拉取一次,之后按 refreshMs 周期触发。
  • 渲染位置:PromptInput.tsx 末尾,输入框 + 快捷键提示行下方,busy / permission prompt 等状态都保持可见。
  • 修改配置需重启 CLI 生效(暂不热加载)。

改动清单

核心(packages/core

  • src/settings.ts
    • 新增类型:StatusLineProviderConfigStatusLineSettingsResolvedStatusLineSettings
    • 新增常量:DEFAULT_STATUSLINE_REFRESH_MS = 2000MIN_STATUSLINE_REFRESH_MS = 500DEFAULT_STATUSLINE_SEPARATOR = " · "
    • 新增 normalizeStatusLineProvider / normalizeStatusLine / mergeStatusLine
    • resolveSettingsSources 返回值新增 statusline 字段
  • src/index.ts:导出新增的三个类型

CLI(packages/cli

  • 新增 src/ui/statusline/
    • types.tsSessionInfoStatusSegmentStatusProviderContextStatusProviderStatusProviderFactory
    • manager.tsStatusLineManager —— 周期拉取、provider 隔离、segment 派发
    • command-provider.ts:执行 shell 命令
    • module-provider.ts:动态 import + 路径白名单校验
    • sanitize.ts:ANSI / 空白 / 长度规范化
    • index.ts:barrel export
  • 新增 src/ui/hooks/useStatusLine.ts:React hook 封装 manager 生命周期
  • src/ui/hooks/index.ts:导出 useStatusLine
  • src/ui/views/App.tsx
    • 新增 getSessionInfo useCallback,组装 SessionInfo(含 model/thinking/reasoningEffort、getCompactPromptTokenThreshold(model) 算出的 context 上限、toolUsagemeta.function.name 聚合)
    • 接入 useStatusLine(resolvedSettings.statusline, projectRoot, getSessionInfo)
    • statusLineSegments + separator 透传给 <PromptInput>
  • src/ui/views/PromptInput.tsx
    • props 新增 statusLineSegments?: StatusSegment[]; statusLineSeparator?: string
    • 末尾渲染分段 + 分隔符 + 颜色
  • 新增 src/tests/statusline.test.ts:覆盖 normalize / merge / manager 行为

文档

  • docs/statusline.md / docs/statusline_en.md:完整功能说明
  • docs/configuration.md / docs/configuration_en.md:在 settings.json 字段表中加入 statusline 一行,引用上面的详细文档

示例插件(.deepcode/plugins/

随 PR 附带一份参考实现,让上游用户开箱可用:

文件 输出示例 说明
model-info.mjs deepseek-v4-pro [thinking:max] 当前模型 + thinking 状态/强度
cwd.mjs ~/projects/foo $HOME 替换为 ~ 的工作目录
git-branch.mjs git:main execFileSyncgit,非 git 仓库返回空字符串
session-stats.mjs msgs:87 reqs:12 ctx:24% 124k/512k 消息/请求计数 + context 百分比 + token 数
tool-usage.mjs bash×21 edit×7 read×3 … 工具调用频次(top 6),固定顺序优先 + 频次降序

测试

  • npm test -w @vegamo/deepcode-cli:218 通过 / 0 失败(5 个直接相关用例:normalize、provider 过滤、manager 拉取/禁用/隔离失败 provider)
  • npm run typecheck:core + cli 干净通过
  • npm run bundlepackages/cli/dist/cli.js 构建成功
  • 本地手动验证:5 个 provider 全部正确渲染,颜色生效,超时/出错 provider 不影响其它段

兼容性

  • 完全向后兼容:未在 settings.json 配置 statusline 字段时,resolveSettings 返回 { enabled: false, providers: [] }useStatusLine 不启动 manager,PromptInput 不渲染状态栏行。
  • 不引入新依赖(只用了 Node 内置 child_process / node:fs / 动态 import())。

在输入框下方渲染由用户配置的状态栏,通过 settings.json#statusline
声明 command/module 两类 provider,无需改动 CLI 源码即可扩展。

- core: 新增 StatusLineProviderConfig/Settings 类型与 normalize/merge 逻辑,
  resolveSettings 返回 ResolvedStatusLineSettings
- cli: 新增 statusline manager、command-provider、module-provider、sanitize;
  useStatusLine hook;App.tsx 组装 SessionInfo(含 model/thinking/context/
  toolUsage);PromptInput.tsx 末尾渲染分段
- docs: 新增 statusline.md / statusline_en.md,configuration 表格补字段
- .deepcode/plugins: 提供 model-info / cwd / git-branch / session-stats /
  tool-usage 五个示例 mjs
- eslint: 为 .deepcode/plugins/**/*.mjs 添加 Node 环境 globals
- tests: 新增 statusline.test.ts,覆盖 normalize / merge / manager
@dengmik-commits

dengmik-commits commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author
image 加了类似 claude code cli 插件的状态栏,效果如上所示。

@dengmik-commits

Copy link
Copy Markdown
Contributor Author

初衷:有时候想知道当前工作目录是什么,要往上翻看启动时的画面,才能知道。这很不方便。
还有当前上下文的情况,也不能掌握。
所以,就参照 claude code cli 做一个

…same file

When the CLI is launched from ~ (home directory), user-level and
project-level settings.json resolve to the same file. This caused
resolveSettingsSources to merge the same content twice, resulting
in duplicate statusline segments and React non-unique key warnings.

Fix: detect same-file early in resolveCurrentSettings and pass null
for project settings when paths are identical.
@dengmik-commits

Copy link
Copy Markdown
Contributor Author

🔧 追加修复:用户级和项目级 settings 同文件时 statusline 重复

问题: 从 ~ 目录启动 CLI 时,userPathprojectPath 指向同一个 ~/.deepcode/settings.jsonresolveSettingsSources 将同一份配置合并两次,导致:

  1. statusline 的 5 个 provider 被复制成 10 段 → 显示重复(如 deepseek-v4-pro · ~ · no session · deepseek-v4-pro · ~ · no session
  2. React 报 Non-unique keys may cause children to be duplicated

修复:resolveCurrentSettings 中提前检测:如果 path.resolve(userPath) === path.resolve(projectPath),传 null 作为 project settings,避免整个合并管线处理同一份文件两次。

 export function resolveCurrentSettings(projectRoot: string = process.cwd()): ResolvedDeepcodingSettings {
+  const userPath = path.resolve(getUserSettingsPath());
+  const projectPath = path.resolve(getProjectSettingsPath(projectRoot));
+  const sameFile = userPath === projectPath;
   return resolveSettingsSources(
     readSettings(),
-    readProjectSettings(projectRoot),
+    sameFile ? null : readProjectSettings(projectRoot),
  • typecheck: ✅ 全部通过(core + cli)
  • tests: ✅ 45/45 通过

dengmik-commits and others added 5 commits June 23, 2026 19:05
- Project-level providers override user-level by id instead of appending
- Add newLine option to break statusline into multiple rows
- Remove external deepcode-core from esbuild (bundle inline)
- Fix permissions.test.ts readonly array type errors
Adopt upstream's packages: "bundle" + code splitting approach
which supersedes our removal of external: ["@vegamo/deepcode-core"].
Add PermissionSettings import and use explicit type annotations for
settings objects with empty arrays (as const produces readonly never[]
which is incompatible with Required<PermissionSettings>).
Keep our Required<PermissionSettings> + as PermissionScope[] approach
which is more explicit than upstream's bare [] fix.
@qorzj qorzj merged commit f279c83 into lessweb:main Jun 25, 2026
6 checks passed
@dengmik-commits dengmik-commits deleted the feat/statusline branch June 25, 2026 08:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants