@caph42/pi-ccline 0.2.2 → 0.2.4

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
@@ -91,7 +91,7 @@ After startup the footer is whatever ccline renders. Your terminal needs Nerd Fo
91
91
  | `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 |
92
92
  | `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 |
93
93
  | `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) |
94
- | `statusBadges` | `{}` | Icons and colors for the status row other extensions set through `ctx.ui.setStatus`, shaped `{"<status key>": {"icon": "<glyph>", "color": "<theme token>"}}`. Built in: `pi-herdr-pstack` (its `poteto-mode` status) → the Nerd Font rocket `U+F135` in `accent`. Fields merge over the built-in entry, so `{"pi-herdr-pstack": {"icon": ""}}` drops the icon and keeps the color; `""` for both removes the badge. Status keys are matched verbatim against `setStatus` keys. A status whose text already carries ANSI colors is left untouched, and an unknown theme token falls back to uncolored text |
94
+ | `statusBadges` | `{}` | Icons and colors for the status row other extensions set through `ctx.ui.setStatus`, shaped `{"<status key>": {"icon": "<glyph>", "color": "<theme token>"}}`. Built in: `pi-herdr-pstack` (its `poteto-mode` status) → magic wand `U+F0D0` in `accent`, `telegram` → `U+F2C6` in `accent`, and `search` (pi-search's working row, e.g. `Search | gpt-6-luna | think:high`, plus its `Docs Search` / `Context7 …` rows that share the key) → magnifier `U+F002` in `mdLink`. Fields merge over the built-in entry, so `{"pi-herdr-pstack": {"icon": ""}}` drops the icon and keeps the color; `""` for both removes the badge. Status keys are matched verbatim against `setStatus` keys. The icon is always added; the color applies to the whole text when the status is plain, and only to the icon when the supplying extension already colored its own text (pi-telegram does). An unknown theme token falls back to uncolored text |
95
95
 
96
96
  Binary resolution order: `config.binary` → `~/.claude/ccline/ccline` → `ccline` on `PATH`.
97
97
 
@@ -104,7 +104,7 @@ Binary resolution order: `config.binary` → `~/.claude/ccline/ccline` → `ccli
104
104
  - Binary not found: the footer shows a one-line install hint.
105
105
  - Widths are always computed with `visibleWidth` / `truncateToWidth`, never with `.length`.
106
106
  - **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.
107
- - **A status key can carry a badge**: `setStatus` hands over text, and whether it is styled depends on the extension that sends it (pi's own footer adds nothing). This footer therefore decorates only statuses that arrive unstyled — a Nerd Font icon and a theme color, per status key (`DEFAULT_STATUS_BADGES` in `config.ts` holds the built-in entry for pi-herdr-pstack's `poteto-mode`). The icon is counted with the rest of the row before truncating, a status whose text already contains ANSI is skipped to avoid mixing two palettes, and the color goes through one `theme.fg()` call per badge that degrades to plain text when the token is not in the theme.
107
+ - **A status key can carry a badge**: `setStatus` hands over text, and whether it is styled depends on the extension that sends it (pi's own footer adds nothing). This footer adds a Nerd Font icon and a theme color per status key (`DEFAULT_STATUS_BADGES` in `config.ts` holds the entries for pi-herdr-pstack's `poteto-mode`, for pi-telegram, and for pi-search's `search` row). The icon is counted with the rest of the row before truncating. When the status text already contains ANSI, only the icon takes the badge color so the sending extension keeps the palette it chose, and the color goes through one `theme.fg()` call per badge that degrades to plain text when the token is not in the theme.
108
108
  - 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.
109
109
  - **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`.
110
110
  - The input JSON **omits the `version` field**: ccline 2.0.1 validates it as a Claude Code version and rejects pi's version outright.
package/README_zh.md CHANGED
@@ -91,7 +91,7 @@ pi install .
91
91
  | `liveRefreshMs` | `1000` | agent 运行期间的定时刷新间隔(让 `耗时` 逐秒走);`0` 关闭定时器,只保留事件刷新。非 0 时低于 250 会被抬到 250 |
92
92
  | `showProvider` | `true` | 是否在模型段追加 provider,如 `Sonnet 4.5 (anthropic)`;`false` 只显示模型名。终端窄到模型段被 ccline 截断时 provider 可能不可见 |
93
93
  | `env` | `{}` | 追加给 ccline 的环境变量;同名变量覆盖 pi 继承的值(`COLUMNS` 除外,它由扩展按当前宽度写入) |
94
- | `statusBadges` | `{}` | 给其他扩展 `ctx.ui.setStatus` 那一行加图标和颜色,形状 `{"<status key>": {"icon": "<字形>", "color": "<主题 token>"}}`。内置一条:`pi-herdr-pstack`(它的 `poteto-mode` 状态)→ Nerd Font 火箭 `U+F135` + `accent`。字段按项合并到内置条目上,所以 `{"pi-herdr-pstack": {"icon": ""}}` 只去掉图标、保留颜色;两个字段都写 `""` 即去掉徽标。status key 与 `setStatus` 的 key 逐字匹配;文本本身已带 ANSI 颜色的状态原样保留;主题里不存在的 token 回退成不上色 |
94
+ | `statusBadges` | `{}` | 给其他扩展 `ctx.ui.setStatus` 那一行加图标和颜色,形状 `{"<status key>": {"icon": "<字形>", "color": "<主题 token>"}}`。内置三条:`pi-herdr-pstack`(它的 `poteto-mode` 状态)→ 魔法棒 `U+F0D0` + `accent`,`telegram` → `U+F2C6` + `accent`,`search`(pi-search 的工作行,如 `Search | gpt-6-luna | think:high`,以及共用这个 key 的 `Docs Search` / `Context7 …` 行)→ 放大镜 `U+F002` + `mdLink`。字段按项合并到内置条目上,所以 `{"pi-herdr-pstack": {"icon": ""}}` 只去掉图标、保留颜色;两个字段都写 `""` 即去掉徽标。status key 与 `setStatus` 的 key 逐字匹配。图标一律加;颜色在文本没上色时刷整段,在发它的扩展已经自己上色时只刷图标(pi-telegram 就是自己上色的)。主题里不存在的 token 回退成不上色 |
95
95
 
96
96
  二进制解析顺序:`config.binary` → `~/.claude/ccline/ccline` → `PATH` 中的 `ccline`。
97
97
 
@@ -104,7 +104,7 @@ pi install .
104
104
  - 找不到二进制:footer 显示一行安装提示。
105
105
  - 行宽一律用 `visibleWidth` / `truncateToWidth` 计算,不用 `.length`。
106
106
  - **其他扩展的 `ctx.ui.setStatus` 照样可见**:`render(width)` 读 `footerData.getExtensionStatuses()`,按 key 排序、用空格拼成一行、超宽时用 dim `…` 截断,追加在 ccline 帧下方——行为对齐 pi 内置 footer。读的是实时 map,`setStatus` 自己会触发重绘,所以状态变化不受 1Hz 节流限制。
107
- - **status 可以带徽标**:`setStatus` 交过来的是文本,有没有上色取决于发它的扩展(pi 自带 footer 不再加任何样式)。所以本扩展只装饰「没有自己上色」的状态:按 status key 加 Nerd Font 图标和主题色(内置条目在 `config.ts` 的 `DEFAULT_STATUS_BADGES`,目前是 pi-herdr-pstack 的 `poteto-mode`)。图标计入整行的可见宽度再截断;文本自带 ANSI 的状态跳过装饰,避免两套配色混在一起;颜色每帧走一次 `theme.fg()`,token 不在主题里就退成纯文本。
107
+ - **status 可以带徽标**:`setStatus` 交过来的是文本,有没有上色取决于发它的扩展(pi 自带 footer 不再加任何样式)。所以本扩展按 status key 加 Nerd Font 图标和主题色(内置条目在 `config.ts` 的 `DEFAULT_STATUS_BADGES`,目前是 pi-herdr-pstack 的 `poteto-mode`、pi-telegram,以及 pi-search 的 `search` 行)。图标计入整行的可见宽度再截断;文本自带 ANSI 时只给图标上色,发它的扩展保留自己选的配色;颜色每帧走一次 `theme.fg()`,token 不在主题里就退成纯文本。
108
108
  - 因为 footer 槽位全局只有一个,本扩展装上后其他扩展的自定义 footer(例如 pi-cursor-provider 的 fast 徽标)会被顶掉;它们的 status 行仍会显示。反过来它们抢占时 ccline 帧会消失,需要 `/pi-ccline on` 取回。
109
109
  - **provider 只能拼进模型段。** ccline 2.0.1 的输入 schema 没有 provider 字段,`model` 段只渲染 `display_name`,所以 provider 以 `名称 (provider)` 的形式追加在模型名后;名字里已经带同样的后缀时不重复追加。`showProvider: false` 可关掉。**窄宽度下 provider 可能看不到**:ccline 按 `COLUMNS` 自己折行/截断,实测宽度 40 时模型段只剩 `GPT-5.6`,这是上游布局行为,本扩展只保证把 provider 写进 `display_name`。
110
110
  - 输入 JSON **省略 `version` 字段**:ccline 2.0.1 会把它当 Claude Code 版本校验,传 pi 的版本会被直接拒绝。
@@ -63,11 +63,28 @@ test("ships a built-in poteto-mode badge and lets the config override it field b
63
63
 
64
64
  // Clearing only the icon keeps the built-in color.
65
65
  const noIcon = normalizeConfig({ statusBadges: { "pi-herdr-pstack": { icon: "" } } });
66
- assert.deepEqual(resolveStatusBadges(noIcon.statusBadges), { "pi-herdr-pstack": { icon: "", color: "accent" } });
66
+ assert.deepEqual(resolveStatusBadges(noIcon.statusBadges), {
67
+ ...DEFAULT_STATUS_BADGES,
68
+ "pi-herdr-pstack": { icon: "", color: "accent" },
69
+ });
67
70
 
68
71
  // A color-only override reuses the built-in icon.
69
72
  const recolored = normalizeConfig({ statusBadges: { "pi-herdr-pstack": { color: "warning" } } });
70
- assert.deepEqual(resolveStatusBadges(recolored.statusBadges), { "pi-herdr-pstack": { icon: DEFAULT_STATUS_BADGES["pi-herdr-pstack"]?.icon, color: "warning" } });
73
+ assert.deepEqual(resolveStatusBadges(recolored.statusBadges), {
74
+ ...DEFAULT_STATUS_BADGES,
75
+ "pi-herdr-pstack": { icon: DEFAULT_STATUS_BADGES["pi-herdr-pstack"]?.icon, color: "warning" },
76
+ });
77
+ });
78
+
79
+ test("ships a built-in badge for pi-search's plain search row", () => {
80
+ const resolved = resolveStatusBadges(undefined);
81
+ assert.deepEqual(resolved.search, { icon: "\uf002", color: "mdLink" });
82
+ // pi-search drives the same key for its docs/context7 rows, so the badge is
83
+ // attached to the key only and a user override still merges field by field.
84
+ assert.deepEqual(resolveStatusBadges(normalizeConfig({ statusBadges: { search: { icon: "" } } }).statusBadges).search, {
85
+ icon: "",
86
+ color: "mdLink",
87
+ });
71
88
  });
72
89
 
73
90
  test("badges can be added for other status keys, and unusable entries are dropped", () => {
@@ -58,7 +58,14 @@ export type StatusBadgeOverride = Partial<StatusBadge>;
58
58
  */
59
59
  export const DEFAULT_STATUS_BADGES: Record<string, StatusBadge> = {
60
60
  // pi-herdr-pstack keeps this key while the sticky poteto methodology is on.
61
- "pi-herdr-pstack": { icon: "\uf135", color: "accent" },
61
+ "pi-herdr-pstack": { icon: "\uf0d0", color: "accent" },
62
+ // pi-telegram colors its own label and state, so only the icon takes the color.
63
+ telegram: { icon: "\uf2c6", color: "accent" },
64
+ // pi-search uses one key for its whole working row: `Search | <model> |
65
+ // think:<level>` while a search runs, and `Docs Search` / `Context7 …` for the
66
+ // docs tools, so one magnifier covers all of them. The text is plain, hence
67
+ // the whole row takes the color; blue keeps it apart from the accent badges.
68
+ search: { icon: "\uf002", color: "mdLink" },
62
69
  };
63
70
 
64
71
  /** An icon longer than this is a typo or a whole path, not a glyph. */
@@ -212,6 +212,31 @@ test("the built-in poteto-mode badge decorates the pi-herdr-pstack status", () =
212
212
  harness.footer.dispose();
213
213
  });
214
214
 
215
+ test("pi-telegram's colored status keeps its colors and gains the icon", () => {
216
+ // lib/status.ts renders `accent(label) muted|warning|success(state)`, so the
217
+ // badge may only prepend its own icon.
218
+ const telegram = "\u001b[36mtelegram\u001b[39m \u001b[32mconnected\u001b[39m";
219
+ const harness = attach({ binary: fakeBinary("cat > /dev/null; printf 'F\\n'") }, undefined, new Map([["telegram", telegram]]));
220
+ const row = harness.render(120)[1] ?? "";
221
+ assert.equal(row.replace(/\u001b\[[0-9;]*m/g, ""), `${DEFAULT_STATUS_BADGES.telegram?.icon} telegram connected`);
222
+ assert.ok(row.includes(telegram), "the plugin's own styling survives");
223
+ harness.footer.dispose();
224
+ });
225
+
226
+ test("pi-search's plain search row gets the built-in magnifier badge", () => {
227
+ // pi-search sends `Search | <model> | think:<level>` unstyled and clears it
228
+ // when the tool returns, so the row may only be decorated, never rewritten.
229
+ const harness = attach(
230
+ { binary: fakeBinary("cat > /dev/null; printf 'FAKE-CCLINE\\n'") },
231
+ undefined,
232
+ new Map([["search", "Search | gpt-6-luna | think:high"]]),
233
+ );
234
+ const row = harness.render(120)[1] ?? "";
235
+ assert.equal(row.replace(/\u001b\[[0-9;]*m/g, ""), `${DEFAULT_STATUS_BADGES.search?.icon} Search | gpt-6-luna | think:high`);
236
+ assert.match(row, /\u001b\[2m/, "the whole row takes the badge color through theme.fg");
237
+ harness.footer.dispose();
238
+ });
239
+
215
240
  test("a configured badge override reaches the rendered row", () => {
216
241
  const statuses = new Map([["pi-herdr-pstack", "poteto-mode"]]);
217
242
  const harness = attach(
@@ -102,13 +102,22 @@ test("a badge adds its icon and color to the matching status key only", () => {
102
102
  assert.equal(line, "connected [accent]R poteto-mode");
103
103
  });
104
104
 
105
- test("a status that colors itself keeps its own styling", () => {
105
+ test("a status that colors itself keeps its colors and gets a colored icon", () => {
106
106
  const statuses = new Map([["pi-herdr-pstack", `${RED}poteto-mode${RESET}`]]);
107
107
  const line = extensionStatusLine(statuses, 80, "…", {
108
108
  badges: { "pi-herdr-pstack": { icon: "R", color: "accent" } },
109
109
  colorize: (color, text) => `[${color}]${text}`,
110
110
  });
111
- assert.equal(line, `${RED}poteto-mode${RESET}`);
111
+ assert.equal(line, `[accent]R ${RED}poteto-mode${RESET}`);
112
+ });
113
+
114
+ test("a self-colored status is not touched at all when its badge has no icon", () => {
115
+ const styled = `${RED}poteto-mode${RESET}`;
116
+ const line = extensionStatusLine(new Map([["pi-herdr-pstack", styled]]), 80, "…", {
117
+ badges: { "pi-herdr-pstack": { icon: "", color: "accent" } },
118
+ colorize: (color, text) => `[${color}]${text}`,
119
+ });
120
+ assert.equal(line, styled);
112
121
  });
113
122
 
114
123
  test("an unknown theme token falls back to the plain badge label", () => {
@@ -47,9 +47,9 @@ export interface StatusLineOptions {
47
47
 
48
48
  /**
49
49
  * A status arrives from another extension as text, and that extension may or
50
- * may not have colored it. A badge adds an icon and a color — but only to a
51
- * status that did not style itself, so an extension that already uses
52
- * `theme.fg` keeps its own palette instead of ending up half-colored.
50
+ * may not have colored it. A badge adds an icon and a color. When the text
51
+ * styles itself the badge colors only its own icon, so the sending extension
52
+ * keeps the palette it chose instead of ending up recolored.
53
53
  */
54
54
  function withBadge(key: string, text: string, options: StatusLineOptions): string {
55
55
  const badges = options.badges;
@@ -57,16 +57,21 @@ function withBadge(key: string, text: string, options: StatusLineOptions): strin
57
57
  // object's inherited members.
58
58
  const badge = badges && Object.hasOwn(badges, key) ? badges[key] : undefined;
59
59
  if (!badge) return text;
60
- if (text.includes("\u001b[")) return text;
61
- const label = badge.icon ? `${badge.icon} ${text}` : text;
62
60
  const colorize = options.colorize;
63
- if (!badge.color || !colorize) return label;
64
- try {
65
- return colorize(badge.color, label);
66
- } catch {
67
- // An unknown theme token must not cost the row its text.
68
- return label;
61
+ const paint = (target: string): string => {
62
+ if (!badge.color || !colorize) return target;
63
+ try {
64
+ return colorize(badge.color, target);
65
+ } catch {
66
+ // An unknown theme token must not cost the row its text.
67
+ return target;
68
+ }
69
+ };
70
+ if (text.includes("\u001b[")) {
71
+ if (!badge.icon) return text;
72
+ return `${paint(badge.icon)} ${text}`;
69
73
  }
74
+ return paint(badge.icon ? `${badge.icon} ${text}` : text);
70
75
  }
71
76
 
72
77
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caph42/pi-ccline",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Pi extension that renders the footer by delegating to the CCometixLine (ccline) statusline binary",
5
5
  "keywords": [
6
6
  "pi-package",