@caph42/pi-ccline 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,68 +1,70 @@
1
1
  # pi-ccline
2
2
 
3
- 把 pi 的 footer 交给原版 [CCometixLine](https://github.com/Haleclipse/CCometixLine)(`ccline`)渲染。
3
+ English | [简体中文](./README_zh.md)
4
4
 
5
- 这个扩展不做任何渲染:它从 pi 会话采集状态,拼成 Claude Code `statusLine` 形状的 JSON,喂给 `ccline` 二进制的 stdin,再把它的 stdout 当成 footer 显示。配色、主题、图标、折行全部由上游决定;上游升级或改主题,这里不需要跟着改。
5
+ Hands pi's footer over to upstream [CCometixLine](https://github.com/Haleclipse/CCometixLine) (`ccline`) to render.
6
+
7
+ This extension does no rendering itself: it collects state from the pi session, assembles it into JSON shaped like Claude Code's `statusLine` input, feeds it to the `ccline` binary's stdin, and shows its stdout as the footer. Colors, themes, icons and wrapping are all decided upstream; when upstream upgrades or changes a theme, nothing here needs to follow.
6
8
 
7
9
  ```
8
- pi 会话状态 → buildCclineInput() → JSON → ccline(spawn,COLUMNS=当前宽度)→ ANSI 行 → footer
10
+ pi session state → buildCclineInput() → JSON → ccline (spawn, COLUMNS=current width) → ANSI line → footer
9
11
  ```
10
12
 
11
- ## 能力
13
+ ## What it shows
12
14
 
13
- | 显示内容 | 来源 |
15
+ | What you see | Where it comes from |
14
16
  |---|---|
15
- | 模型名(含 provider) | `ctx.model`(`display_name` 取 `model.name`,回退 `model.id`;`showProvider` 为真时追加 ` (provider)`,如 `Sonnet 4.5 (anthropic)`) |
16
- | 推理强度 | `ctx.thinkingLevel` → `effort.level` |
17
- | 工作目录 | `ctx.cwd` → `cwd` / `workspace.current_dir` |
18
- | 上下文百分比 / token 数 | `ctx.getContextUsage()` → 当前上下文(聚合估计值);**不发会话累计 token** |
19
- | 成本、会话时长 | assistant 消息 `usage.cost.total` 累加;时长 = 当前时间 − 分支最早 entry |
20
- | 会话 id / 名称 | `ctx.sessionManager` |
21
- | git 分支等 ccline 自行探测的信息 | ccline 自己读磁盘 |
22
- | 其他扩展的 status | `footerData.getExtensionStatuses()`,按 key 排序拼成一行放在 ccline 帧下方 |
17
+ | Model name (with provider) | `ctx.model` (`display_name` takes `model.name`, falling back to `model.id`; when `showProvider` is true it appends ` (provider)`, e.g. `Sonnet 4.5 (anthropic)`) |
18
+ | Reasoning effort | `ctx.thinkingLevel` → `effort.level` |
19
+ | Working directory | `ctx.cwd` → `cwd` / `workspace.current_dir` |
20
+ | Context percentage / token count | `ctx.getContextUsage()` → current context (the aggregate estimate); **no lifetime session tokens are sent** |
21
+ | Cost, session duration | assistant messages' `usage.cost.total` summed; duration = now − earliest entry on the branch |
22
+ | Session id / name | `ctx.sessionManager` |
23
+ | git branch and anything else ccline probes itself | ccline reads the disk itself |
24
+ | Status lines from other extensions | `footerData.getExtensionStatuses()`, sorted by key into one line below the ccline frame |
23
25
 
24
- 不显示、也不会伪造的:5h / 7d 配额段(pi 没有 `rate_limits` 数据源,字段不传,该段自动消失)。
26
+ Not shown, and never fabricated: the 5h / 7d quota segment (pi has no `rate_limits` source, the field is omitted, and the segment disappears by itself).
25
27
 
26
- ## 安装
28
+ ## Installation
27
29
 
28
- 先准备外部二进制(扩展不自带渲染器,必须有它):
30
+ Start with the external binary (the extension ships no renderer, so this is required):
29
31
 
30
32
  ```sh
31
33
  npm i -g @cometix/ccline
32
- # 或者只放一份到 ~/.claude/ccline/ccline
34
+ # or drop a single copy at ~/.claude/ccline/ccline
33
35
  ```
34
36
 
35
- **已在 ccline 2.0.1 上验证。** 更老的 1.1.x 会把 `model` / `workspace` / `transcript_path` 当必填字段,收到这里的输入会直接报
36
- `missing field ...` 并非 0 退出(footer 会停在上一帧并标 stale),所以旧安装请先升级。
37
+ **Verified against ccline 2.0.1.** Older 1.1.x treats `model` / `workspace` / `transcript_path` as required fields and exits non-zero with
38
+ `missing field ...` on the input built here (the footer then stays on its previous frame and is marked stale), so upgrade first.
37
39
 
38
- 再装 pi 包(任选其一):
40
+ Then install the pi package (pick one):
39
41
 
40
42
  ```sh
41
- # 从 npm 正式安装(会写 ~/.pi/agent/settings.json)
43
+ # install from npm for real (writes ~/.pi/agent/settings.json)
42
44
  pi install npm:@caph42/pi-ccline
43
45
 
44
- # 从 npm 试用一次,不写 settings.json
46
+ # try it from npm once, without touching settings.json
45
47
  pi -e npm:@caph42/pi-ccline
46
48
 
47
- # 本地源码试用 / 正式安装(在包根目录执行)
49
+ # try / install from local source (run in the package root)
48
50
  pi -e .
49
51
  pi install .
50
52
  ```
51
53
 
52
- 启动后 footer 直接变成 ccline 的渲染结果。终端需要 Nerd Font v3+ 才能显示图标;`style.mode = "plain"` 的 ccline 主题可以退化成 emoji。
54
+ After startup the footer is whatever ccline renders. Your terminal needs Nerd Font v3+ to show the icons; ccline themes with `style.mode = "plain"` degrade to emoji.
53
55
 
54
- ## 命令
56
+ ## Commands
55
57
 
56
- | 命令 | 作用 |
58
+ | Command | Effect |
57
59
  |---|---|
58
- | `/pi-ccline on` | 打开 footer(写入配置,立即生效) |
59
- | `/pi-ccline off` | 关闭并恢复 pi 自带 footer(写入配置) |
60
- | `/pi-ccline status` | 启用状态、模式、解析到的二进制、节流/超时/定时刷新、刷新次数、上一帧是否 stale |
61
- | `/pi-ccline path` | 解析到的二进制路径、来源(config/home/path)与 `ccline --version` |
60
+ | `/pi-ccline on` | Turn the footer on (writes config, takes effect immediately) |
61
+ | `/pi-ccline off` | Turn it off and restore pi's built-in footer (writes config) |
62
+ | `/pi-ccline status` | Enabled state, mode, resolved binary, throttle / timeout / live refresh, refresh count, whether the last frame is stale |
63
+ | `/pi-ccline path` | Resolved binary path, its source (config/home/path) and `ccline --version` |
62
64
 
63
- ## 配置
65
+ ## Configuration
64
66
 
65
- `~/.pi/agent/pi-ccline/config.json`(文件不存在、JSON 坏了、字段类型不对,一律回退默认值,不报错):
67
+ `~/.pi/agent/pi-ccline/config.json` (a missing file, broken JSON or wrongly typed fields all fall back to the defaults silently):
66
68
 
67
69
  ```json
68
70
  {
@@ -76,80 +78,80 @@ pi install .
76
78
  }
77
79
  ```
78
80
 
79
- | 字段 | 默认 | 说明 |
81
+ | Field | Default | Meaning |
80
82
  |---|---|---|
81
- | `enabled` | `true` | 会话启动时是否安装 footer |
82
- | `binary` | `null` | 显式指定 ccline 路径;`null` 走下面的解析顺序。路径不存在时静默回退 |
83
- | `timeoutMs` | `5000` | 单次调用的硬超时(200–60000) |
84
- | `refreshThrottleMs` | `1000` | 两次刷新最小间隔,低于 1000 会被抬到 1000 |
85
- | `liveRefreshMs` | `1000` | agent 运行期间的定时刷新间隔(让 `耗时` 逐秒走);`0` 关闭定时器,只保留事件刷新。非 0 时低于 250 会被抬到 250 |
86
- | `showProvider` | `true` | 是否在模型段追加 provider,如 `Sonnet 4.5 (anthropic)`;`false` 只显示模型名。终端窄到模型段被 ccline 截断时 provider 可能不可见 |
87
- | `env` | `{}` | 追加给 ccline 的环境变量;同名变量覆盖 pi 继承的值(`COLUMNS` 除外,它由扩展按当前宽度写入) |
88
-
89
- 二进制解析顺序:`config.binary` → `~/.claude/ccline/ccline` → `PATH` 中的 `ccline`。
90
-
91
- ## 实现要点
92
-
93
- - `render(width)` 是同步热路径,**绝不在里面 spawn**:只返回缓存帧;宽度变化只记录并投递一次刷新请求。
94
- - 刷新由 `session_start` / `turn_start` / `turn_end` / `model_select` / `thinking_level_select` / `session_info_changed` / git 分支变化触发,经 `RefreshScheduler` 单飞 + 节流 + latest-wins:任何时刻最多一个子进程,排队请求合并为一次,读的是发起时刻的最新状态。
95
- - **耗时这类随时间变化的段靠定时器推进**:纯事件刷新只能在事件发生时算一次「现在 − 起点」,所以 agent 运行期间(`agent_start` → `agent_end`)另有 `RefreshTicker` 按 `liveRefreshMs` 投递刷新请求,`耗时` 才会逐秒走;run 结束即停表,之后只有一次已排队的刷新可能落地,再没有周期刷新。下一轮的 `turn_start` 会重新起表;如果在 run 途中才装上/打开 footer,`attach()` 读 `ctx.isIdle()` 后直接起表。
96
- - 超时 5s(可配)或非 0 退出:保留上一帧并在行尾加 ` ·stale`(放得下才加),不刷屏、不抛错。
97
- - 找不到二进制:footer 显示一行安装提示。
98
- - 行宽一律用 `visibleWidth` / `truncateToWidth` 计算,不用 `.length`。
99
- - **其他扩展的 `ctx.ui.setStatus` 照样可见**:`render(width)` 读 `footerData.getExtensionStatuses()`,按 key 排序、用空格拼成一行、超宽时用 dim `…` 截断,追加在 ccline 帧下方——行为对齐 pi 内置 footer。读的是实时 map,`setStatus` 自己会触发重绘,所以状态变化不受 1Hz 节流限制。
100
- - 因为 footer 槽位全局只有一个,本扩展装上后其他扩展的自定义 footer(例如 pi-cursor-provider 的 fast 徽标)会被顶掉;它们的 status 行仍会显示。反过来它们抢占时 ccline 帧会消失,需要 `/pi-ccline on` 取回。
101
- - **provider 只能拼进模型段。** ccline 2.0.1 的输入 schema 没有 provider 字段,`model` 段只渲染 `display_name`,所以 provider 以 `名称 (provider)` 的形式追加在模型名后;名字里已经带同样的后缀时不重复追加。`showProvider: false` 可关掉。**窄宽度下 provider 可能看不到**:ccline 按 `COLUMNS` 自己折行/截断,实测宽度 40 时模型段只剩 `GPT-5.6`,这是上游布局行为,本扩展只保证把 provider 写进 `display_name`。
102
- - 输入 JSON **省略 `version` 字段**:ccline 2.0.1 会把它当 Claude Code 版本校验,传 pi 的版本会被直接拒绝。
103
- - 没有运行时依赖,只用 `node:child_process` / `node:fs` / `node:os` / `node:path` 和 pi 提供的 peer 包 `@earendil-works/pi-tui`。
104
- - 非 TUI 模式(`-p`、json、rpc)不安装 footer、不 spawn。
105
- - 扩展自己的配置写在 `~/.pi/agent/pi-ccline/config.json`,不碰 pi 的 `settings.json`。
106
-
107
- ## 验证
83
+ | `enabled` | `true` | Whether to install the footer when a session starts |
84
+ | `binary` | `null` | Explicit ccline path; `null` follows the resolution order below. A path that doesn't exist falls back silently |
85
+ | `timeoutMs` | `5000` | Hard timeout per invocation (200–60000) |
86
+ | `refreshThrottleMs` | `1000` | Minimum interval between refreshes; anything below 1000 is raised to 1000 |
87
+ | `liveRefreshMs` | `1000` | Interval of the timer that refreshes while the agent runs (so `duration` ticks every second); `0` disables the timer and leaves only event-driven refreshes. When non-zero, anything below 250 is raised to 250 |
88
+ | `showProvider` | `true` | Whether to append the provider to the model segment, e.g. `Sonnet 4.5 (anthropic)`; `false` shows just the model name. If the terminal is narrow enough for ccline to truncate the model segment, the provider may not be visible |
89
+ | `env` | `{}` | Extra environment variables for ccline; same-named variables override the values inherited from pi (except `COLUMNS`, which the extension writes from the current width) |
90
+
91
+ Binary resolution order: `config.binary` → `~/.claude/ccline/ccline` → `ccline` on `PATH`.
92
+
93
+ ## Implementation notes
94
+
95
+ - `render(width)` is a synchronous hot path and **never spawns anything** from inside: it returns the cached frame; a width change is only recorded and one refresh request is dispatched.
96
+ - Refreshes are triggered by `session_start` / `turn_start` / `turn_end` / `model_select` / `thinking_level_select` / `session_info_changed` / a git branch change, and go through `RefreshScheduler` with single-flight + throttling + latest-wins: at most one child process at any moment, queued requests collapse into one, and it reads the freshest state as of the moment it starts.
97
+ - **Segments that move with time, like `duration`, are advanced by a timer**: a purely event-driven refresh can only compute "now − start" once, when the event fires, so while the agent runs (`agent_start` → `agent_end`) a `RefreshTicker` dispatches refresh requests at `liveRefreshMs`, which is what makes `duration` tick every second; the clock stops when the run ends, after which at most one already-queued refresh can land and no periodic refresh remains. The next `turn_start` restarts the clock; if the footer is installed or switched on mid-run, `attach()` reads `ctx.isIdle()` and starts it immediately.
98
+ - A timeout (5s, configurable) or a non-zero exit keeps the previous frame and appends ` ·stale` at the end of the line (only if it fits): no flooding, no throwing.
99
+ - Binary not found: the footer shows a one-line install hint.
100
+ - Widths are always computed with `visibleWidth` / `truncateToWidth`, never with `.length`.
101
+ - **Other extensions' `ctx.ui.setStatus` stays visible**: `render(width)` reads `footerData.getExtensionStatuses()`, sorts by key, joins them with spaces into one line, truncates with a dim `…` when too wide, and appends it below the ccline frame, matching pi's built-in footer. It reads the live map and `setStatus` itself triggers a repaint, so status changes are not limited by the 1Hz throttle.
102
+ - Because there is only one footer slot globally, other extensions' custom footers (e.g. pi-cursor-provider's fast badge) get displaced once this extension is installed; their status lines still show. Conversely, when they take over, the ccline frame disappears and `/pi-ccline on` takes it back.
103
+ - **The provider can only be folded into the model segment.** ccline 2.0.1's input schema has no provider field and the `model` segment renders only `display_name`, so the provider is appended after the model name as `name (provider)`; it isn't appended twice when the name already carries the same suffix. `showProvider: false` turns it off. **At narrow widths the provider may be invisible**: ccline wraps/truncates by `COLUMNS` itself, and in practice at width 40 the model segment is left with just `GPT-5.6` — that's upstream layout behaviour, and this extension only guarantees the provider is written into `display_name`.
104
+ - The input JSON **omits the `version` field**: ccline 2.0.1 validates it as a Claude Code version and rejects pi's version outright.
105
+ - No runtime dependencies: only `node:child_process` / `node:fs` / `node:os` / `node:path` plus the pi peer package `@earendil-works/pi-tui`.
106
+ - Non-TUI modes (`-p`, json, rpc) install no footer and spawn nothing.
107
+ - The extension's own config lives at `~/.pi/agent/pi-ccline/config.json`; pi's `settings.json` is never touched.
108
+
109
+ ## Verification
108
110
 
109
111
  ```sh
110
- # 单元测试(输入映射、宽度截断、节流/latest-wins、二进制解析、超时、footer 生命周期)
112
+ # unit tests (input mapping, width truncation, throttle/latest-wins, binary resolution, timeout, footer lifecycle)
111
113
  npm test
112
114
 
113
- # 用 fixture 跑真实 ccline,宽 120 与 40 各一次(打印可见宽度,验证不越界)
115
+ # run the real ccline against the fixture at width 120 and 40 (prints visible widths, checks nothing overflows)
114
116
  node --experimental-strip-types scripts/render-once.ts --fixture fixtures/basic.json
115
117
 
116
- # 只跑某个宽度 / 指定二进制 / 隔离 HOME
118
+ # single width / pinned binary / raw ANSI
117
119
  node --experimental-strip-types scripts/render-once.ts --width 40 --binary ~/.claude/ccline/ccline --raw
118
120
 
119
- # 完全隔离:配置从该 HOME 读,子进程 HOME 也指向它(state 不会写进你的 ~/.claude)
121
+ # fully isolated: config is read from this HOME and the child's HOME points there too (state never lands in your ~/.claude)
120
122
  node --experimental-strip-types scripts/render-once.ts --home "$(mktemp -d)" --binary ~/.claude/ccline/ccline
121
123
  ```
122
124
 
123
- `--home` 会把该目录同时用于三处:查 `~/.claude/ccline/ccline`、读 `<home>/.pi/agent/pi-ccline/config.json`、以及给子进程设置 `HOME`/`USERPROFILE`。如果解析到的是 npm 包装脚本(`@cometix/ccline/bin/ccline.js`),它可能自己往这个 HOME 里铺主题等资源,想得到干净结果就同时用 `--binary` 钉死真实二进制。
125
+ `--home` is used for three things at once: finding `~/.claude/ccline/ccline`, reading `<home>/.pi/agent/pi-ccline/config.json`, and setting the child's `HOME`/`USERPROFILE`. If what resolves is the npm wrapper script (`@cometix/ccline/bin/ccline.js`), it may populate that HOME with themes and other resources itself; pin the real binary with `--binary` as well when you want a clean result.
124
126
 
125
- `scripts/render-once.ts` 不启动 pi、不发模型请求,只验证「输入 → 二进制 → 行」这一段。
127
+ `scripts/render-once.ts` starts no pi and sends no model requests; it verifies only the "input → binary → line" stretch.
126
128
 
127
- ## 已知限制
129
+ ## Known limitations
128
130
 
129
- - **5h / 7d 配额段永远为空。** pi 不提供 `rate_limits`,字段整段省略,ccline 自然不显示。
130
- - **ccline 会写 `~/.claude/ccline/state/`**(会话状态、模型缓存、update.lock)。这是上游行为,扩展无法关闭。
131
- - **依赖外部二进制。** 没有 `ccline` 时 footer 只有一行安装提示;升级 `ccline` 后无需重启扩展,但二进制路径会被缓存到下一次解析(`/pi-ccline on` 或新会话会重算)。
132
- - **上游兼容性只覆盖已测版本。** ccline 2.0.1 的 `input.rs` 每个字段都是 `Option` + lenient 反序列化,同一大版本内字段换形状一般只会让对应段消失;但大版本之间已经发生过必填字段变化(1.1.x → 2.0.x),升级 ccline 后请用 `scripts/render-once.ts` 复跑一次。
133
- - **需要 Nerd Font v3+。** 图标字体缺失时会显示成方块;Windows 未测试。
134
- - **上下文 token 只有一套口径:当前上下文。** `context_window` 只发 `current_usage.input_tokens`(= `getContextUsage().tokens`)和 `used_percentage`,不发 `total_input_tokens` / `total_output_tokens`。原因是 ccline 优先取累计值,那样进度条百分比会和 token 数互相矛盾(长会话显示成 `15% · 512k/272k`)。代价:会话累计输入/输出 token 不出现在 footer 里;成本与时长仍来自 `cost` 段。pi 只给聚合估计值,没有 input/cache 拆分,所以 `current_usage` 里只填 `input_tokens`。
135
- - **分支视角。** 汇总只覆盖 `getBranch()` 可见的当前分支;compaction 之后仍按分支上现存记录计算,不等于「整个会话生命周期总量」。切分支后数字会变。
136
- - **不传 `transcript_path`。** pi 的 session jsonl 不是 Claude Code 的 transcript 格式,传过去只会让上游读到无法解析的内容;因此在那个位置上显示的信息来自 JSON 本身而不是重新解析会话文件。
137
- - **刷新频率上限 1Hz。** 流式输出期间上下文用量变化不会逐 token 刷新,只在节流窗口后更新一次。
138
- - **`耗时` 只在 agent 运行期间逐秒走。** 定时器随 run 起停,空闲时停在最后一刻的值,直到下一个事件(发消息、切模型、切分支等)再刷新;`liveRefreshMs` 只控制运行期间的刷新请求节奏,不开启空闲刷新。`liveRefreshMs` 与 `refreshThrottleMs` 都是 1000 时,稳态约 1s 一次,实际间隔受两者共同限制。若以后想让空闲也逐秒走,需要改 `agent_end` 的停表逻辑,属于待评估的耗电取舍。
139
- - **每次定时刷新都是一次进程创建。** 本机实测 spawn ccline 约 24ms(二进制自身 4.4ms),所以运行期间约每秒一次;`liveRefreshMs: 0` 可以彻底关掉。
140
- - **首次刷新可能用 100/120 列。** 首帧宽度优先取 `process.stdout.columns`,拿不到时用 120,pi 第一次绘制后会按真实宽度重刷一次(多一次 4ms 级 spawn)。
131
+ - **The 5h / 7d quota segment is always empty.** pi provides no `rate_limits`, the field is omitted entirely, and ccline shows nothing.
132
+ - **ccline writes `~/.claude/ccline/state/`** (session state, model cache, update.lock). That's upstream behaviour and cannot be disabled here.
133
+ - **External binary required.** Without `ccline` the footer is a single install hint; upgrading `ccline` needs no extension restart, but the binary path stays cached until the next resolution (`/pi-ccline on` or a new session recomputes it).
134
+ - **Upstream compatibility only covers tested versions.** Every field in ccline 2.0.1's `input.rs` is `Option` with lenient deserialization, so a field changing shape within the same major version usually just makes that segment disappear; but required fields have changed across majors before (1.1.x → 2.0.x), so re-run `scripts/render-once.ts` after upgrading ccline.
135
+ - **Nerd Font v3+ required.** Missing icon fonts render as boxes; Windows is untested.
136
+ - **There is only one measure of context tokens: the current context.** `context_window` sends only `current_usage.input_tokens` (= `getContextUsage().tokens`) and `used_percentage`, never `total_input_tokens` / `total_output_tokens`. The reason is that ccline prefers the lifetime values, which makes the progress-bar percentage contradict the token count (a long session shows up as `15% · 512k/272k`). The cost: lifetime input/output tokens don't appear in the footer; cost and duration still come from the `cost` segment. pi provides only the aggregate estimate, with no input/cache split, so only `input_tokens` is filled in `current_usage`.
137
+ - **Branch perspective.** The aggregate covers only the current branch as seen by `getBranch()`; after compaction it is still computed from the records that remain on that branch, which is not the same as "total for the whole session lifetime". Switching branches changes the numbers.
138
+ - **`transcript_path` is not sent.** pi's session jsonl is not Claude Code's transcript format; passing it would only make upstream read something it cannot parse, so whatever appears in that spot comes from the JSON itself rather than a re-parse of the session file.
139
+ - **Refresh rate is capped at 1Hz.** During streaming, context usage is not refreshed token by token; it updates once after the throttle window.
140
+ - **`duration` only ticks while the agent runs.** The timer starts and stops with the run, so while idle it sits at the last value until the next event (sending a message, switching model, switching branch, …); `liveRefreshMs` only controls the cadence of refresh requests during a run, it does not enable idle refreshes. With both `liveRefreshMs` and `refreshThrottleMs` at 1000, the steady state is roughly once per second, with the actual interval bounded by both. Making idle time tick every second would mean changing the stop logic in `agent_end` — an untested power trade-off.
141
+ - **Every timer tick is a process creation.** On this machine spawning ccline measured about 24ms (4.4ms in the binary itself), hence roughly once per second during a run; `liveRefreshMs: 0` turns it off entirely.
142
+ - **The first refresh may use 100/120 columns.** The first frame prefers `process.stdout.columns` and falls back to 120; after pi's first paint it refreshes once at the real width (one extra ~4ms spawn).
141
143
 
142
- ## 目录
144
+ ## Layout
143
145
 
144
146
  ```
145
147
  extensions/pi-ccline/
146
- index.ts 生命周期、setFooter、命令、CclineFooter(缓存帧 + 单飞刷新)
147
- input.ts pi 状态 → ccline 输入 JSON(纯函数)
148
- binary.ts 路径解析/缓存、带超时与中止的 spawn、--version 探测
149
- render.ts 输出归一化:分行、按可见宽度截断、stale 标记
150
- refresh.ts 单飞 + 节流 + latest-wins 调度器,以及驱动 `耗时` 的定时器
151
- config.ts 配置读写
148
+ index.ts lifecycle, setFooter, commands, CclineFooter (cached frame + single-flight refresh)
149
+ input.ts pi state → ccline input JSON (pure functions)
150
+ binary.ts path resolution/caching, spawn with timeout and abort, --version probing
151
+ render.ts output normalization: line splitting, truncation by visible width, stale marker
152
+ refresh.ts single-flight + throttle + latest-wins scheduler, and the timer that drives `duration`
153
+ config.ts config read/write
152
154
  *.test.ts node --test
153
- scripts/render-once.ts 用 fixture 跑一次真实 ccline
154
- fixtures/basic.json 人工验收用输入
155
+ scripts/render-once.ts run the real ccline once against a fixture
156
+ fixtures/basic.json input used for manual acceptance
155
157
  ```
package/README_zh.md ADDED
@@ -0,0 +1,157 @@
1
+ # pi-ccline
2
+
3
+ [English](./README.md) | 简体中文
4
+
5
+ 把 pi 的 footer 交给原版 [CCometixLine](https://github.com/Haleclipse/CCometixLine)(`ccline`)渲染。
6
+
7
+ 这个扩展不做任何渲染:它从 pi 会话采集状态,拼成 Claude Code `statusLine` 形状的 JSON,喂给 `ccline` 二进制的 stdin,再把它的 stdout 当成 footer 显示。配色、主题、图标、折行全部由上游决定;上游升级或改主题,这里不需要跟着改。
8
+
9
+ ```
10
+ pi 会话状态 → buildCclineInput() → JSON → ccline(spawn,COLUMNS=当前宽度)→ ANSI 行 → footer
11
+ ```
12
+
13
+ ## 能力
14
+
15
+ | 显示内容 | 来源 |
16
+ |---|---|
17
+ | 模型名(含 provider) | `ctx.model`(`display_name` 取 `model.name`,回退 `model.id`;`showProvider` 为真时追加 ` (provider)`,如 `Sonnet 4.5 (anthropic)`) |
18
+ | 推理强度 | `ctx.thinkingLevel` → `effort.level` |
19
+ | 工作目录 | `ctx.cwd` → `cwd` / `workspace.current_dir` |
20
+ | 上下文百分比 / token 数 | `ctx.getContextUsage()` → 当前上下文(聚合估计值);**不发会话累计 token** |
21
+ | 成本、会话时长 | assistant 消息 `usage.cost.total` 累加;时长 = 当前时间 − 分支最早 entry |
22
+ | 会话 id / 名称 | `ctx.sessionManager` |
23
+ | git 分支等 ccline 自行探测的信息 | ccline 自己读磁盘 |
24
+ | 其他扩展的 status | `footerData.getExtensionStatuses()`,按 key 排序拼成一行放在 ccline 帧下方 |
25
+
26
+ 不显示、也不会伪造的:5h / 7d 配额段(pi 没有 `rate_limits` 数据源,字段不传,该段自动消失)。
27
+
28
+ ## 安装
29
+
30
+ 先准备外部二进制(扩展不自带渲染器,必须有它):
31
+
32
+ ```sh
33
+ npm i -g @cometix/ccline
34
+ # 或者只放一份到 ~/.claude/ccline/ccline
35
+ ```
36
+
37
+ **已在 ccline 2.0.1 上验证。** 更老的 1.1.x 会把 `model` / `workspace` / `transcript_path` 当必填字段,收到这里的输入会直接报
38
+ `missing field ...` 并非 0 退出(footer 会停在上一帧并标 stale),所以旧安装请先升级。
39
+
40
+ 再装 pi 包(任选其一):
41
+
42
+ ```sh
43
+ # 从 npm 正式安装(会写 ~/.pi/agent/settings.json)
44
+ pi install npm:@caph42/pi-ccline
45
+
46
+ # 从 npm 试用一次,不写 settings.json
47
+ pi -e npm:@caph42/pi-ccline
48
+
49
+ # 本地源码试用 / 正式安装(在包根目录执行)
50
+ pi -e .
51
+ pi install .
52
+ ```
53
+
54
+ 启动后 footer 直接变成 ccline 的渲染结果。终端需要 Nerd Font v3+ 才能显示图标;`style.mode = "plain"` 的 ccline 主题可以退化成 emoji。
55
+
56
+ ## 命令
57
+
58
+ | 命令 | 作用 |
59
+ |---|---|
60
+ | `/pi-ccline on` | 打开 footer(写入配置,立即生效) |
61
+ | `/pi-ccline off` | 关闭并恢复 pi 自带 footer(写入配置) |
62
+ | `/pi-ccline status` | 启用状态、模式、解析到的二进制、节流/超时/定时刷新、刷新次数、上一帧是否 stale |
63
+ | `/pi-ccline path` | 解析到的二进制路径、来源(config/home/path)与 `ccline --version` |
64
+
65
+ ## 配置
66
+
67
+ `~/.pi/agent/pi-ccline/config.json`(文件不存在、JSON 坏了、字段类型不对,一律回退默认值,不报错):
68
+
69
+ ```json
70
+ {
71
+ "enabled": true,
72
+ "binary": null,
73
+ "timeoutMs": 5000,
74
+ "refreshThrottleMs": 1000,
75
+ "liveRefreshMs": 1000,
76
+ "showProvider": true,
77
+ "env": {}
78
+ }
79
+ ```
80
+
81
+ | 字段 | 默认 | 说明 |
82
+ |---|---|---|
83
+ | `enabled` | `true` | 会话启动时是否安装 footer |
84
+ | `binary` | `null` | 显式指定 ccline 路径;`null` 走下面的解析顺序。路径不存在时静默回退 |
85
+ | `timeoutMs` | `5000` | 单次调用的硬超时(200–60000) |
86
+ | `refreshThrottleMs` | `1000` | 两次刷新最小间隔,低于 1000 会被抬到 1000 |
87
+ | `liveRefreshMs` | `1000` | agent 运行期间的定时刷新间隔(让 `耗时` 逐秒走);`0` 关闭定时器,只保留事件刷新。非 0 时低于 250 会被抬到 250 |
88
+ | `showProvider` | `true` | 是否在模型段追加 provider,如 `Sonnet 4.5 (anthropic)`;`false` 只显示模型名。终端窄到模型段被 ccline 截断时 provider 可能不可见 |
89
+ | `env` | `{}` | 追加给 ccline 的环境变量;同名变量覆盖 pi 继承的值(`COLUMNS` 除外,它由扩展按当前宽度写入) |
90
+
91
+ 二进制解析顺序:`config.binary` → `~/.claude/ccline/ccline` → `PATH` 中的 `ccline`。
92
+
93
+ ## 实现要点
94
+
95
+ - `render(width)` 是同步热路径,**绝不在里面 spawn**:只返回缓存帧;宽度变化只记录并投递一次刷新请求。
96
+ - 刷新由 `session_start` / `turn_start` / `turn_end` / `model_select` / `thinking_level_select` / `session_info_changed` / git 分支变化触发,经 `RefreshScheduler` 单飞 + 节流 + latest-wins:任何时刻最多一个子进程,排队请求合并为一次,读的是发起时刻的最新状态。
97
+ - **耗时这类随时间变化的段靠定时器推进**:纯事件刷新只能在事件发生时算一次「现在 − 起点」,所以 agent 运行期间(`agent_start` → `agent_end`)另有 `RefreshTicker` 按 `liveRefreshMs` 投递刷新请求,`耗时` 才会逐秒走;run 结束即停表,之后只有一次已排队的刷新可能落地,再没有周期刷新。下一轮的 `turn_start` 会重新起表;如果在 run 途中才装上/打开 footer,`attach()` 读 `ctx.isIdle()` 后直接起表。
98
+ - 超时 5s(可配)或非 0 退出:保留上一帧并在行尾加 ` ·stale`(放得下才加),不刷屏、不抛错。
99
+ - 找不到二进制:footer 显示一行安装提示。
100
+ - 行宽一律用 `visibleWidth` / `truncateToWidth` 计算,不用 `.length`。
101
+ - **其他扩展的 `ctx.ui.setStatus` 照样可见**:`render(width)` 读 `footerData.getExtensionStatuses()`,按 key 排序、用空格拼成一行、超宽时用 dim `…` 截断,追加在 ccline 帧下方——行为对齐 pi 内置 footer。读的是实时 map,`setStatus` 自己会触发重绘,所以状态变化不受 1Hz 节流限制。
102
+ - 因为 footer 槽位全局只有一个,本扩展装上后其他扩展的自定义 footer(例如 pi-cursor-provider 的 fast 徽标)会被顶掉;它们的 status 行仍会显示。反过来它们抢占时 ccline 帧会消失,需要 `/pi-ccline on` 取回。
103
+ - **provider 只能拼进模型段。** ccline 2.0.1 的输入 schema 没有 provider 字段,`model` 段只渲染 `display_name`,所以 provider 以 `名称 (provider)` 的形式追加在模型名后;名字里已经带同样的后缀时不重复追加。`showProvider: false` 可关掉。**窄宽度下 provider 可能看不到**:ccline 按 `COLUMNS` 自己折行/截断,实测宽度 40 时模型段只剩 `GPT-5.6`,这是上游布局行为,本扩展只保证把 provider 写进 `display_name`。
104
+ - 输入 JSON **省略 `version` 字段**:ccline 2.0.1 会把它当 Claude Code 版本校验,传 pi 的版本会被直接拒绝。
105
+ - 没有运行时依赖,只用 `node:child_process` / `node:fs` / `node:os` / `node:path` 和 pi 提供的 peer 包 `@earendil-works/pi-tui`。
106
+ - 非 TUI 模式(`-p`、json、rpc)不安装 footer、不 spawn。
107
+ - 扩展自己的配置写在 `~/.pi/agent/pi-ccline/config.json`,不碰 pi 的 `settings.json`。
108
+
109
+ ## 验证
110
+
111
+ ```sh
112
+ # 单元测试(输入映射、宽度截断、节流/latest-wins、二进制解析、超时、footer 生命周期)
113
+ npm test
114
+
115
+ # 用 fixture 跑真实 ccline,宽 120 与 40 各一次(打印可见宽度,验证不越界)
116
+ node --experimental-strip-types scripts/render-once.ts --fixture fixtures/basic.json
117
+
118
+ # 只跑某个宽度 / 指定二进制 / 隔离 HOME
119
+ node --experimental-strip-types scripts/render-once.ts --width 40 --binary ~/.claude/ccline/ccline --raw
120
+
121
+ # 完全隔离:配置从该 HOME 读,子进程 HOME 也指向它(state 不会写进你的 ~/.claude)
122
+ node --experimental-strip-types scripts/render-once.ts --home "$(mktemp -d)" --binary ~/.claude/ccline/ccline
123
+ ```
124
+
125
+ `--home` 会把该目录同时用于三处:查 `~/.claude/ccline/ccline`、读 `<home>/.pi/agent/pi-ccline/config.json`、以及给子进程设置 `HOME`/`USERPROFILE`。如果解析到的是 npm 包装脚本(`@cometix/ccline/bin/ccline.js`),它可能自己往这个 HOME 里铺主题等资源,想得到干净结果就同时用 `--binary` 钉死真实二进制。
126
+
127
+ `scripts/render-once.ts` 不启动 pi、不发模型请求,只验证「输入 → 二进制 → 行」这一段。
128
+
129
+ ## 已知限制
130
+
131
+ - **5h / 7d 配额段永远为空。** pi 不提供 `rate_limits`,字段整段省略,ccline 自然不显示。
132
+ - **ccline 会写 `~/.claude/ccline/state/`**(会话状态、模型缓存、update.lock)。这是上游行为,扩展无法关闭。
133
+ - **依赖外部二进制。** 没有 `ccline` 时 footer 只有一行安装提示;升级 `ccline` 后无需重启扩展,但二进制路径会被缓存到下一次解析(`/pi-ccline on` 或新会话会重算)。
134
+ - **上游兼容性只覆盖已测版本。** ccline 2.0.1 的 `input.rs` 每个字段都是 `Option` + lenient 反序列化,同一大版本内字段换形状一般只会让对应段消失;但大版本之间已经发生过必填字段变化(1.1.x → 2.0.x),升级 ccline 后请用 `scripts/render-once.ts` 复跑一次。
135
+ - **需要 Nerd Font v3+。** 图标字体缺失时会显示成方块;Windows 未测试。
136
+ - **上下文 token 只有一套口径:当前上下文。** `context_window` 只发 `current_usage.input_tokens`(= `getContextUsage().tokens`)和 `used_percentage`,不发 `total_input_tokens` / `total_output_tokens`。原因是 ccline 优先取累计值,那样进度条百分比会和 token 数互相矛盾(长会话显示成 `15% · 512k/272k`)。代价:会话累计输入/输出 token 不出现在 footer 里;成本与时长仍来自 `cost` 段。pi 只给聚合估计值,没有 input/cache 拆分,所以 `current_usage` 里只填 `input_tokens`。
137
+ - **分支视角。** 汇总只覆盖 `getBranch()` 可见的当前分支;compaction 之后仍按分支上现存记录计算,不等于「整个会话生命周期总量」。切分支后数字会变。
138
+ - **不传 `transcript_path`。** pi 的 session jsonl 不是 Claude Code 的 transcript 格式,传过去只会让上游读到无法解析的内容;因此在那个位置上显示的信息来自 JSON 本身而不是重新解析会话文件。
139
+ - **刷新频率上限 1Hz。** 流式输出期间上下文用量变化不会逐 token 刷新,只在节流窗口后更新一次。
140
+ - **`耗时` 只在 agent 运行期间逐秒走。** 定时器随 run 起停,空闲时停在最后一刻的值,直到下一个事件(发消息、切模型、切分支等)再刷新;`liveRefreshMs` 只控制运行期间的刷新请求节奏,不开启空闲刷新。`liveRefreshMs` 与 `refreshThrottleMs` 都是 1000 时,稳态约 1s 一次,实际间隔受两者共同限制。若以后想让空闲也逐秒走,需要改 `agent_end` 的停表逻辑,属于待评估的耗电取舍。
141
+ - **每次定时刷新都是一次进程创建。** 本机实测 spawn ccline 约 24ms(二进制自身 4.4ms),所以运行期间约每秒一次;`liveRefreshMs: 0` 可以彻底关掉。
142
+ - **首次刷新可能用 100/120 列。** 首帧宽度优先取 `process.stdout.columns`,拿不到时用 120,pi 第一次绘制后会按真实宽度重刷一次(多一次 4ms 级 spawn)。
143
+
144
+ ## 目录
145
+
146
+ ```
147
+ extensions/pi-ccline/
148
+ index.ts 生命周期、setFooter、命令、CclineFooter(缓存帧 + 单飞刷新)
149
+ input.ts pi 状态 → ccline 输入 JSON(纯函数)
150
+ binary.ts 路径解析/缓存、带超时与中止的 spawn、--version 探测
151
+ render.ts 输出归一化:分行、按可见宽度截断、stale 标记
152
+ refresh.ts 单飞 + 节流 + latest-wins 调度器,以及驱动 `耗时` 的定时器
153
+ config.ts 配置读写
154
+ *.test.ts node --test
155
+ scripts/render-once.ts 用 fixture 跑一次真实 ccline
156
+ fixtures/basic.json 人工验收用输入
157
+ ```
@@ -1,7 +1,7 @@
1
1
  {
2
- "cwd": "/Users/caph/Workspace/pi-packages/packages/pi-ccline",
2
+ "cwd": "/Users/caph/Workspace/pi-packages/pi-ccline",
3
3
  "workspace": {
4
- "current_dir": "/Users/caph/Workspace/pi-packages/packages/pi-ccline"
4
+ "current_dir": "/Users/caph/Workspace/pi-packages/pi-ccline"
5
5
  },
6
6
  "model": {
7
7
  "id": "gpt-5.6-codex",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caph42/pi-ccline",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Pi extension that renders the footer by delegating to the CCometixLine (ccline) statusline binary",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -28,7 +28,8 @@
28
28
  "extensions",
29
29
  "scripts",
30
30
  "fixtures",
31
- "README.md"
31
+ "README.md",
32
+ "README_zh.md"
32
33
  ],
33
34
  "pi": {
34
35
  "extensions": [