@bachi/pi-coder 1.0.0

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.
Files changed (101) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +162 -0
  4. package/config/AGENTS.md +100 -0
  5. package/config/pi-statusline.json +140 -0
  6. package/config/settings.json +38 -0
  7. package/config/web-search.json +5 -0
  8. package/docs/README.md +14 -0
  9. package/docs/configuration.md +123 -0
  10. package/docs/development.md +177 -0
  11. package/docs/extensions.md +292 -0
  12. package/docs/handbook.zh.md +432 -0
  13. package/docs/installation.md +124 -0
  14. package/docs/themes.md +107 -0
  15. package/extensions/ask-user-question/answers.test.ts +104 -0
  16. package/extensions/ask-user-question/answers.ts +72 -0
  17. package/extensions/ask-user-question/dialog.test.ts +180 -0
  18. package/extensions/ask-user-question/dialog.ts +102 -0
  19. package/extensions/ask-user-question/index.ts +253 -0
  20. package/extensions/ask-user-question/model.test.ts +275 -0
  21. package/extensions/ask-user-question/model.ts +259 -0
  22. package/extensions/ask-user-question/schema.ts +49 -0
  23. package/extensions/ask-user-question/types.ts +86 -0
  24. package/extensions/ask-user-question/validate.test.ts +183 -0
  25. package/extensions/ask-user-question/validate.ts +110 -0
  26. package/extensions/ask-user-question/view.ts +262 -0
  27. package/extensions/auto-default-model/default-model.test.ts +268 -0
  28. package/extensions/auto-default-model/index.ts +87 -0
  29. package/extensions/bash-command-collapse.ts +1476 -0
  30. package/extensions/below-editor-after-statusline.ts +118 -0
  31. package/extensions/clear-command.ts +29 -0
  32. package/extensions/cwd-statusline.ts +39 -0
  33. package/extensions/exit-command.ts +59 -0
  34. package/extensions/fenceless-code-block/index.test.ts +208 -0
  35. package/extensions/fenceless-code-block/index.ts +28 -0
  36. package/extensions/fenceless-code-block/render.test.ts +177 -0
  37. package/extensions/fenceless-code-block/render.ts +142 -0
  38. package/extensions/folder-history.ts +197 -0
  39. package/extensions/init-command.ts +163 -0
  40. package/extensions/prompt-editor/bash-prompt.test.ts +94 -0
  41. package/extensions/prompt-editor/bash-prompt.ts +59 -0
  42. package/extensions/prompt-editor/render.test.ts +283 -0
  43. package/extensions/prompt-editor.ts +212 -0
  44. package/extensions/read-path-collapse.ts +474 -0
  45. package/extensions/recap/index.test.ts +348 -0
  46. package/extensions/recap/index.ts +462 -0
  47. package/extensions/recap/subagents.test.ts +144 -0
  48. package/extensions/recap/subagents.ts +128 -0
  49. package/extensions/rewind/README.md +229 -0
  50. package/extensions/rewind/checkpoints.test.ts +560 -0
  51. package/extensions/rewind/checkpoints.ts +820 -0
  52. package/extensions/rewind/flow.test.ts +756 -0
  53. package/extensions/rewind/flow.ts +362 -0
  54. package/extensions/rewind/index.ts +400 -0
  55. package/extensions/rewind/picker.ts +135 -0
  56. package/extensions/rewind/viewport.test.ts +76 -0
  57. package/extensions/rewind/viewport.ts +48 -0
  58. package/extensions/simple-task/gap.test.ts +147 -0
  59. package/extensions/simple-task/gap.ts +122 -0
  60. package/extensions/simple-task/index.ts +439 -0
  61. package/extensions/simple-task/types.ts +53 -0
  62. package/extensions/simple-task/widget.ts +86 -0
  63. package/extensions/startup-logo/header-guard.test.ts +274 -0
  64. package/extensions/startup-logo/header-guard.ts +166 -0
  65. package/extensions/startup-logo/index.test.ts +305 -0
  66. package/extensions/startup-logo/index.ts +194 -0
  67. package/extensions/startup-logo/loaded-sections.test.ts +257 -0
  68. package/extensions/startup-logo/loaded-sections.ts +267 -0
  69. package/extensions/startup-logo/logo.test.ts +124 -0
  70. package/extensions/startup-logo/logo.ts +124 -0
  71. package/extensions/statusline/footer-guard.test.ts +273 -0
  72. package/extensions/statusline/footer-guard.ts +171 -0
  73. package/extensions/statusline/git.test.ts +174 -0
  74. package/extensions/statusline/git.ts +142 -0
  75. package/extensions/statusline/index.ts +294 -0
  76. package/extensions/statusline/line.test.ts +316 -0
  77. package/extensions/statusline/line.ts +201 -0
  78. package/extensions/subagent-log-guard/filter.test.ts +85 -0
  79. package/extensions/subagent-log-guard/filter.ts +32 -0
  80. package/extensions/subagent-log-guard/index.ts +112 -0
  81. package/extensions/theme-command.ts +263 -0
  82. package/extensions/thinking-collapse/window.test.ts +321 -0
  83. package/extensions/thinking-collapse/window.ts +354 -0
  84. package/extensions/thinking-collapse.ts +60 -0
  85. package/extensions/tool-diff/title-row.test.ts +254 -0
  86. package/extensions/tool-diff/title-row.ts +191 -0
  87. package/extensions/tool-diff.ts +1276 -0
  88. package/extensions/working-indicator/bash-spinner.test.ts +135 -0
  89. package/extensions/working-indicator/bash-spinner.ts +114 -0
  90. package/extensions/working-indicator/index.test.ts +579 -0
  91. package/extensions/working-indicator/index.ts +940 -0
  92. package/extensions/working-indicator/spinner-frames.test.ts +219 -0
  93. package/extensions/working-indicator/spinner-frames.ts +156 -0
  94. package/extensions/working-indicator/summary-request.test.ts +195 -0
  95. package/extensions/working-indicator/summary-request.ts +207 -0
  96. package/extensions/working-indicator/working-summary.test.ts +499 -0
  97. package/extensions/working-indicator/working-summary.ts +375 -0
  98. package/package.json +71 -0
  99. package/themes/ayu.json +97 -0
  100. package/themes/catppuccin.json +103 -0
  101. package/themes/summer-night.json +87 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Tests for bash-spinner.ts — working 行尾「bash 执行中 `●`」的形态与并发登记簿。
3
+ *
4
+ * Run with: node --test clients/pi/extensions/working-indicator/bash-spinner.test.ts
5
+ *
6
+ * 被测模块不 import pi / pi-tui,断言直接比对纯文本 / 纯状态。
7
+ */
8
+
9
+ import assert from "node:assert/strict";
10
+ import { describe, it } from "node:test";
11
+
12
+ import {
13
+ BASH_SPINNER_BLINK_MS,
14
+ BASH_SPINNER_COLOR,
15
+ BASH_SPINNER_DELAY_MS,
16
+ BASH_SPINNER_MARKER,
17
+ BASH_SPINNER_WIDTH,
18
+ BashRunTracker,
19
+ bashSpinnerSuffix,
20
+ } from "./bash-spinner.ts";
21
+
22
+ /** 可见宽度:本模块只出现 ASCII 空格与 `●`(U+25CF,1 列),逐字符数即可。 */
23
+ function widthOf(text: string): number {
24
+ return [...text].length;
25
+ }
26
+
27
+ describe("bashSpinnerSuffix", () => {
28
+ it("亮相位 `\" ●\"`、灭相位两格空格(绝不返回空串)", () => {
29
+ assert.equal(bashSpinnerSuffix(true), ` ${BASH_SPINNER_MARKER}`);
30
+ assert.equal(bashSpinnerSuffix(false), " ".repeat(BASH_SPINNER_WIDTH));
31
+ assert.notEqual(bashSpinnerSuffix(false), "");
32
+ });
33
+
34
+ it("两态等宽**且等字符数**(右对齐摘要不随亮灭左右蹦)", () => {
35
+ for (const text of [bashSpinnerSuffix(true), bashSpinnerSuffix(false)]) {
36
+ assert.equal(widthOf(text), BASH_SPINNER_WIDTH);
37
+ assert.equal([...text].length, BASH_SPINNER_WIDTH);
38
+ }
39
+ });
40
+
41
+ it("返回的是**未上色**的纯文本(上色由扩展按色槽做)", () => {
42
+ for (const text of [bashSpinnerSuffix(true), bashSpinnerSuffix(false)]) {
43
+ assert.doesNotMatch(text, /\x1b\[/);
44
+ }
45
+ });
46
+
47
+ it("色槽是 dim(退役的 bash 首行 spinner 同一个槽位)", () => {
48
+ assert.equal(BASH_SPINNER_COLOR, "dim");
49
+ });
50
+ });
51
+
52
+ describe("节拍常量", () => {
53
+ it("门槛 1s、亮灭各 500ms、整除", () => {
54
+ assert.equal(BASH_SPINNER_DELAY_MS, 1000);
55
+ assert.equal(BASH_SPINNER_BLINK_MS, 500);
56
+ assert.equal(BASH_SPINNER_DELAY_MS % BASH_SPINNER_BLINK_MS, 0);
57
+ });
58
+ });
59
+
60
+ describe("BashRunTracker", () => {
61
+ it("没有 bash 在跑:不显示、不需要排程", () => {
62
+ const runs = new BashRunTracker();
63
+ assert.equal(runs.size, 0);
64
+ assert.equal(runs.shown, false);
65
+ assert.equal(runs.markCrossed(0), false);
66
+ assert.equal(runs.armDelay(0), null);
67
+ });
68
+
69
+ it("门槛内不显示,到点(1000ms)即显示", () => {
70
+ const runs = new BashRunTracker();
71
+ runs.start("a", 5_000);
72
+ assert.equal(runs.markCrossed(5_999), false);
73
+ assert.equal(runs.shown, false);
74
+ assert.equal(runs.armDelay(5_999), 1);
75
+ assert.equal(runs.markCrossed(6_000), true);
76
+ assert.equal(runs.shown, true);
77
+ assert.equal(runs.armDelay(6_000), null, "已跨过门槛就不再排程");
78
+ });
79
+
80
+ it("排程时刻 = 最早那个执行的截止点,且第一个执行结束后顺延到下一个", () => {
81
+ const runs = new BashRunTracker();
82
+ runs.start("a", 0);
83
+ runs.start("b", 500);
84
+ assert.equal(runs.armDelay(0), 1_000, "两个都在门槛内:等最早那个");
85
+ runs.end("a");
86
+ assert.equal(runs.armDelay(0), 1_500, "a 结束后等 b");
87
+ assert.equal(runs.armDelay(1_500), 0, "已经到点就是 0,不再往后算");
88
+ });
89
+
90
+ it("粘性:跨过门槛的执行结束后,同批还没到门槛的照旧把标记撑住", () => {
91
+ const runs = new BashRunTracker();
92
+ runs.start("long", 0);
93
+ runs.start("young", 900);
94
+ assert.equal(runs.markCrossed(1_000), true, "long 跨过门槛");
95
+ runs.end("long");
96
+ assert.equal(runs.shown, true, "young 还在跑,标记不能灭");
97
+ assert.equal(runs.markCrossed(1_100), true, "young 本身还没到门槛,靠粘性撑住");
98
+ assert.equal(runs.armDelay(1_100), null, "已跨过门槛,不再为 young 排程");
99
+ runs.end("young");
100
+ assert.equal(runs.shown, false, "所有 bash 都结束 → 标记消失");
101
+ assert.equal(runs.size, 0);
102
+ });
103
+
104
+ it("最后一个结束才消失:同批两个都在门槛之后,先结束一个不影响", () => {
105
+ const runs = new BashRunTracker();
106
+ runs.start("a", 0);
107
+ runs.start("b", 0);
108
+ assert.equal(runs.markCrossed(1_000), true);
109
+ runs.end("a");
110
+ assert.equal(runs.shown, true);
111
+ runs.end("b");
112
+ assert.equal(runs.shown, false);
113
+ });
114
+
115
+ it("换批次不继承粘性标志:新起的短命令得重新自己跨门槛", () => {
116
+ const runs = new BashRunTracker();
117
+ runs.start("a", 0);
118
+ runs.markCrossed(1_000);
119
+ runs.end("a");
120
+ runs.start("b", 2_000);
121
+ assert.equal(runs.markCrossed(2_100), false, "上一批的 sticky 已随登记簿清空复位");
122
+ assert.equal(runs.shown, false);
123
+ assert.equal(runs.armDelay(2_100), 900);
124
+ });
125
+
126
+ it("clear() 清空整本账(换回合 / 会话替换 / 旧 ctx)", () => {
127
+ const runs = new BashRunTracker();
128
+ runs.start("a", 0);
129
+ runs.markCrossed(1_000);
130
+ runs.clear();
131
+ assert.equal(runs.size, 0);
132
+ assert.equal(runs.shown, false);
133
+ assert.equal(runs.armDelay(1_000), null);
134
+ });
135
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * bash-spinner.ts — working 行尾「bash 执行中 `●`」的纯逻辑:形态、门槛、并发登记簿。
3
+ *
4
+ * 抽出来的理由与 `working-summary.ts` 相同:形态(`" ●"` 与等宽空格占位)、色槽、门槛与节拍
5
+ * 常量,以及「多个 bash 并发时谁把标记撑住」这条状态机全是纯判定,不该活在 `index.ts` 的
6
+ * 定时器与 `ctx.ui` 之间。本模块**不 import pi / pi-tui**,`node --test` 直接跑满每个分支
7
+ * (用例见 `bash-spinner.test.ts`);定时器那半边(什么时候闪、什么时候重绘)在扩展里。
8
+ *
9
+ * ## 需求
10
+ *
11
+ * 原文:把 `●` 挂在 `⠼ Tools Calling (13m 59s)` **后面**,bash 执行时长超过 1 秒后出现、
12
+ * 执行结束后消失;多个 bash 同时在执行时,任意一个超过 1s 就闪,**所有** bash 都结束后才消失。
13
+ * 于是:
14
+ * - 门槛与节拍沿用退役的 bash 首行 spinner(门槛 1s、亮 / 灭各 500ms),`●` 不再出现在
15
+ * bash 命令块里(那个首行 `$ ` → `● ` 的替换按需求撤销了);
16
+ * - 「撑住」是**粘性**的(见 `BashRunTracker.markCrossed`):只要有一个执行真的跨过门槛,
17
+ * 标记就保持到**最后一个**执行结束 —— 否则「长命令结束、同批另一个还在跑」时标记会灭一下
18
+ * 再亮,看着像闪断,也不符合「所有 bash 结束后才消失」。
19
+ *
20
+ * ## 形态恒 2 列
21
+ *
22
+ * 亮相位 `" ●"`(前导空格 + 标记)、灭相位两格空格:**恒 2 列、恒 2 字符、绝不返回空串**。
23
+ * 灭相位要是空串,标记每 500ms 亮灭时行尾会少两列,`layoutPromptSummary` 算出的 gap 跟着
24
+ * 变、右对齐的提示词摘要会左右蹦 —— 这就是 `tool-diff` 标题行 `●` 记下的那条教训。同理,
25
+ * 标记段整段参与 `leftWidth`(在 `index.ts` 里拼进左段再交给布局),所以摘要位置不随亮灭移动。
26
+ */
27
+
28
+ /** 出现门槛(毫秒):执行时长达到它才开始闪 `●`。 */
29
+ export const BASH_SPINNER_DELAY_MS = 1000;
30
+ /** 亮 / 灭各占的毫秒数(与 tool-diff 的 `edit` / `write` 同节拍)。 */
31
+ export const BASH_SPINNER_BLINK_MS = 500;
32
+ /** 标记字符。1 列宽(`visibleWidth("●") === 1`,与 statusline / tool-diff 同源,已核对)。 */
33
+ export const BASH_SPINNER_MARKER = "●";
34
+ /** 标记段的可见宽度(= 字符数):前导空格 1 列 + 标记 1 列。 */
35
+ export const BASH_SPINNER_WIDTH = 2;
36
+ /**
37
+ * 主题色槽。用 `dim`(退役的 bash 首行 spinner 同一个槽位):它比 working 行默认的 `muted`
38
+ * 更沉,落在统计段之后是个「次要但可见」的活跃点,不跟正文抢注意力。
39
+ */
40
+ export const BASH_SPINNER_COLOR = "dim";
41
+
42
+ /**
43
+ * 标记段文本(**未上色**,上色在扩展里按 `BASH_SPINNER_COLOR` 做)。
44
+ * `on` = 亮相位 → `" ●"`;灭相位 → 两格空格。两态等宽等字符数(见文件头)。
45
+ */
46
+ export function bashSpinnerSuffix(on: boolean): string {
47
+ return on ? ` ${BASH_SPINNER_MARKER}` : " ".repeat(BASH_SPINNER_WIDTH);
48
+ }
49
+
50
+ /**
51
+ * 并发 bash 执行的登记簿:`toolCallId → 起始时刻`(`tool_execution_start` / `_end` 落账)。
52
+ *
53
+ * 三个入口各管一件事:`start` / `end` 记账、`markCrossed(now)` 观察门槛(并立起粘性标志)、
54
+ * `armDelay(now)` 给「还没到门槛」那段时间算一次性定时器该等多久(让 `●` **准时**在门槛上
55
+ * 出现,而不是等下一次别的重绘 —— 非流式命令执行期间可能一个上游事件都没有)。
56
+ */
57
+ export class BashRunTracker {
58
+ private readonly startedAt = new Map<string, number>();
59
+ /** 本批执行里是否已经有谁跨过门槛(粘性,见文件头;登记簿清空时复位)。 */
60
+ private crossed = false;
61
+
62
+ /** 登记一个开始执行的 bash。 */
63
+ start(toolCallId: string, now: number): void {
64
+ this.startedAt.set(toolCallId, now);
65
+ }
66
+
67
+ /**
68
+ * 注销一个结束的 bash。**最后一个结束**时连同粘性标志一起复位 —— 标记要等所有执行都
69
+ * 结束才消失,且下一批 bash 得重新自己跨门槛,不继承上一批的。
70
+ */
71
+ end(toolCallId: string): void {
72
+ this.startedAt.delete(toolCallId);
73
+ if (this.startedAt.size === 0) this.crossed = false;
74
+ }
75
+
76
+ /** 换回合 / 会话替换 / 旧 ctx:整本账清掉,标记立刻消失。 */
77
+ clear(): void {
78
+ this.startedAt.clear();
79
+ this.crossed = false;
80
+ }
81
+
82
+ /** 当前有几个 bash 在执行。 */
83
+ get size(): number {
84
+ return this.startedAt.size;
85
+ }
86
+
87
+ /** 标记是否该显示(已跨过门槛 **且** 还有执行在跑)。 */
88
+ get shown(): boolean {
89
+ return this.crossed && this.startedAt.size > 0;
90
+ }
91
+
92
+ /**
93
+ * 按当前时刻观察一次:有执行跨过门槛就把粘性标志立起来,返回标记是否该显示。
94
+ * 每个渲染时机(事件 / 读秒 tick / 节拍 tick)都调它。
95
+ */
96
+ markCrossed(now: number): boolean {
97
+ for (const startedAt of this.startedAt.values()) {
98
+ if (now - startedAt >= BASH_SPINNER_DELAY_MS) this.crossed = true;
99
+ }
100
+ return this.shown;
101
+ }
102
+
103
+ /**
104
+ * 最早那个执行跨过门槛还要等多少毫秒(≥ 0);没有执行 / 已经跨过(粘性标志已立)时返回
105
+ * `null` = 不需要排程。最新一个执行的截止时刻永远不早于已有执行,所以一个在途的排程不用
106
+ * 因为后来的 `start` 重排。
107
+ */
108
+ armDelay(now: number): number | null {
109
+ if (this.crossed || this.startedAt.size === 0) return null;
110
+ let earliest = Number.POSITIVE_INFINITY;
111
+ for (const startedAt of this.startedAt.values()) earliest = Math.min(earliest, startedAt);
112
+ return Math.max(0, earliest + BASH_SPINNER_DELAY_MS - now);
113
+ }
114
+ }