@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,375 @@
1
+ /**
2
+ * working-summary.ts — spinner 行右对齐「提示词摘要」的纯文本压平 + 纯布局逻辑。
3
+ *
4
+ * `working-indicator.ts` 在 spinner 文案(`Working (23s)` / `Tools Calling (…)`)的
5
+ * 右侧追加当前回合提示词的摘要(**整段提示词压平成一行**后的前排字符 + `…`),形如:
6
+ *
7
+ * ⠧ Working (23s) ✦ /init 我要优化…
8
+ *
9
+ * 本文件不 import 任何 pi / pi-tui 模块 —— 宽度计算通过 `widthOf` 依赖注入(运行时传
10
+ * pi-tui 的 `visibleWidth`,与渲染器同一套实现,所以量出来的宽度与 pi 实际折行用的宽度
11
+ * 严格一致;单测里喂一个简单的 ASCII=1 / CJK=2 假函数即可),因此能用 `node --test`
12
+ * 直接跑:
13
+ *
14
+ * node --test clients/pi/extensions/working-indicator/working-summary.test.ts
15
+ *
16
+ * ## 压平(`flattenPrompt`)
17
+ *
18
+ * 多行提示词要显示成**一句正常的话**,而不是只显示第一行 —— 第一行往往只有几个字
19
+ * ("帮我看看"、"修一下"),信息量全在后面几行。规则:
20
+ *
21
+ * - 换行、缩进、制表符、连续空格全部折叠:行首行尾空白去掉(缩进消失),行内
22
+ * **≥2 个连续空格折叠成 1 个**,单个空格原样保留(单词之间那一个空格是有意义的)。
23
+ * 刻意不按字面的「超过两个才折叠」实现 —— 那样 3 个空格变 1 个、2 个空格却保留
24
+ * 2 个,越长反而越窄,非单调。
25
+ * - 行间连接符按**两侧字符**决定(`joinSeparator`),目标是读起来像原句:
26
+ * 两侧都是宽字符(CJK) → 不补空格(中文折行处本来就没有空格)
27
+ * 任一侧是 CJK 标点 → 不补空格(`,。:、「」《》` 等两侧都不吃空格)
28
+ * 行尾是开括号/引号 → 不补空格(`call(` + `)` → `call()`)
29
+ * 行首是闭括号/收尾标点 → 不补空格(`summary` + `.ts` → `summary.ts`,路径不断)
30
+ * 其余(含 ASCII 逗号后、中英混排)→ 补 1 个空格(`hello,` + `world` → `hello, world`)
31
+ * - 各种「看不见的脏东西」一并归一:`\r\n` / `\r` / `\u2028` / `\u2029` / `\u0085` /
32
+ * `\v` / `\f` 都当换行;NBSP / 全角空格 / `\u2000-\u200a` 等 Unicode 空格当普通空格;
33
+ * 零宽字符(`\u200b` / `\ufeff` / 双向标记)删掉 —— 它们不参与显示却会混进宽度
34
+ * 计算,把右对齐算歪;ANSI 转义序列(从终端复制粘贴会带)整段剥掉,否则截断可能
35
+ * 切在转义序列中间渲染出乱码;其余控制字符删除。
36
+ * - Markdown 排版噪声剥掉:代码围栏标记行(` ```ts `,**围栏内的代码正文保留**)、
37
+ * 分隔线(`---` / `***` / `___`)、引用标记 `> `、标题标记 `## `、列表标记
38
+ * `- ` / `* ` / `+ ` / `1. `。压成一行后它们只是垃圾字符。
39
+ * - 空行(含段落之间的多个空行)不产生额外空格;以空行开头的提示词也能正常摘要
40
+ * (旧实现取第一行,这种情况直接判定为「无摘要」)。
41
+ * - 只扫描前 `MAX_PROMPT_SCAN_CHARS` 个字符:摘要最多显示半屏,多扫纯属白做功,
42
+ * 而这个函数每个 `input` 事件都要跑一次(提示词可能是几十 KB 的粘贴)。切点若落在
43
+ * 代理对中间,丢掉那个落单的高位代理。
44
+ *
45
+ * 刻意**不做**的两件事(都会误伤正常内容):行尾连字符的「反断词」(`opti-` + `mization`
46
+ * → `optimization`)—— 无法与真的以 `-` 结尾的行区分;以及跨行 URL 的无缝拼接 —— 检测
47
+ * 不可靠,按普通规则补空格。
48
+ *
49
+ * ## 布局(`layoutPromptSummary`)
50
+ *
51
+ * 给定左段可见宽度、整行可见宽度预算与压平后的提示词,算出摘要文本(按可见宽度截断、
52
+ * 必要时补 `…`)和左段与摘要之间的空格数,使摘要**恰好右对齐到行尾**。
53
+ *
54
+ * 为什么右对齐靠「算空格」而不是靠渲染器:pi 的 working message 走
55
+ * `Text` 组件的 word-wrap(`wrapTextWithAnsi`),只有**整行可见宽度 ≤ 折行宽度**
56
+ * 时才原样单行输出(行内空格原样保留);一旦超宽就会在空格处折到第二行、
57
+ * 且行尾空格被 `trimEnd` 掉。所以这里把总宽**恰好**凑满预算:既不折行,
58
+ * 摘要又顶到右端。调用方传入的 `totalWidth` 必须已扣除 spinner 与 Text
59
+ * 左右 padding 占的列数(见 working-indicator.ts 的 `SPINNER_AND_PADDING_COLUMNS`)。
60
+ *
61
+ * 截断规则:针对**整段压平后的提示词**(不是第一行)。放得下就全量展示(不补省略号),
62
+ * 放不下就截到放得下的最长前缀再接一个 `…`。`…` 恒占位(按 `widthOf` 量),截断点落在
63
+ * 宽字符(CJK 占 2 列)中间时宁可少一列也不超宽 —— 少掉的列数自动并进 gap,摘要仍然
64
+ * 右对齐。另有一个独立的**整段宽度上限**(`maxWidth`,调用方传终端宽度的一半):不管
65
+ * 行内剩余空间多大,摘要段(`✦ ` + 正文)都不超过它 —— 免得宽终端上一条长提示词把左半
66
+ * 行的状态文案挤得只剩零星几列;上限收窄出的富余同样并进 gap,右对齐不变。
67
+ *
68
+ * 可用宽度本身(`availableSummaryTextWidth`)是公开的:`summary-request.ts` 在请求模型
69
+ * 摘要时要用同一个数——先算好「这一格能放多少列」,再据此决定值不值得请求、要求模型输出
70
+ * 多长。渲染与请求共用一份宽度数学,模型拿到的目标长度才不会和实际能显示的对不上。
71
+ */
72
+
73
+ /** 可见宽度函数:与 pi-tui `visibleWidth` 同语义(剥 ANSI、宽字符按 2 列)。 */
74
+ export type WidthFn = (text: string) => number;
75
+
76
+ /** 摘要前的标记符。可见宽度在运行时用 `widthOf` 现量,不假设它是 1 列。 */
77
+ export const SUMMARY_MARKER = "✦";
78
+
79
+ /** 左段与摘要之间的最小空隙(列)。右对齐天然把摘要推到行尾,这只是窄终端下的下限。 */
80
+ export const DEFAULT_MIN_GAP = 1;
81
+
82
+ /** 摘要正文(不含 `✦ ` 前缀)至少要有这么宽才值得显示,否则整条摘要省略。 */
83
+ export const MIN_SUMMARY_TEXT_WIDTH = 4;
84
+
85
+ /**
86
+ * 压平时最多扫描多少个原始字符(UTF-16 码元)。摘要最多显示半屏(几百列),
87
+ * 扫描上限只是给「几十 KB 的粘贴」兜底,让 `input` 事件里的那次计算保持廉价。
88
+ */
89
+ export const MAX_PROMPT_SCAN_CHARS = 4000;
90
+
91
+ /** 截断时接在末尾的省略号。 */
92
+ const ELLIPSIS = "…";
93
+
94
+ /* eslint-disable no-control-regex -- 下面几条正则必须匹配控制字符 / 转义序列本身 */
95
+
96
+ /** ANSI / VT 转义序列:CSI(`ESC [ … 终止符`)、OSC(`ESC ] … BEL|ST`)、以及双字符转义。 */
97
+ const ANSI_RE =
98
+ /\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)|\u001b[@-Z\\-_]/g;
99
+
100
+ /** 零宽 / 不可见格式字符:显示上不占位,留着会污染宽度计算。 */
101
+ const ZERO_WIDTH_RE = /[\u200b\u200c\u200d\u200e\u200f\u2060\u2061-\u2064\ufeff]/g;
102
+
103
+ /** Unicode 空白(不含 ASCII 空格与制表符):一律归一成普通空格。 */
104
+ const UNICODE_SPACE_RE = /[\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000]/g;
105
+
106
+ /** 所有当换行处理的字符:LF / CR / CRLF / NEL / VT / FF / 行分隔符 / 段分隔符。 */
107
+ const LINE_BREAK_RE = /\r\n|[\n\r\u0085\u000b\u000c\u2028\u2029]/g;
108
+
109
+ /** 行内剩余的控制字符(换行与制表符已在上一步处理掉)。 */
110
+ const CONTROL_RE = /[\u0000-\u001f\u007f]/g;
111
+
112
+ /* eslint-enable no-control-regex */
113
+
114
+ /** 代码围栏标记行(` ``` ` / `~~~`,可带语言标识):整行丢掉,围栏内的正文保留。 */
115
+ const FENCE_LINE_RE = /^(?:`{3,}|~{3,})[^\s`~]*$/;
116
+ /** 水平分隔线(`---` / `***` / `___`,可带空格):整行丢掉。 */
117
+ const RULE_LINE_RE = /^(?:[-*_]\s*){3,}$/;
118
+ /** 引用标记 `> `(可嵌套 `> > `)。 */
119
+ const BLOCKQUOTE_RE = /^(?:>\s?)+/;
120
+ /** ATX 标题标记 `# ` … `###### `。 */
121
+ const HEADING_RE = /^#{1,6}\s+/;
122
+ /** 列表标记:`- ` / `* ` / `+ ` / `• ` 与 `1. ` / `2) `(标记后必须有空白,避免误伤 `-1` / `**粗体**`)。 */
123
+ const LIST_BULLET_RE = /^(?:[-*+•·]\s+|\d{1,3}[.)]\s+)/;
124
+
125
+ /** 行内 ≥2 个连续空格折叠成 1 个(单个空格保留)。 */
126
+ const SPACE_RUN_RE = / {2,}/g;
127
+
128
+ /**
129
+ * 宽字符(终端占 2 列):CJK 表意文字、假名、谚文、全角形式等。
130
+ * 与 `joinSeparator` 的「两侧都是宽字符就不补空格」配套 —— 口径只需覆盖
131
+ * 「中日韩正文」,不必与 wcwidth 表逐格一致(差几格只影响要不要补一个空格)。
132
+ */
133
+ function isWideChar(ch: string): boolean {
134
+ const c = ch.codePointAt(0) ?? 0;
135
+ return (
136
+ (c >= 0x1100 && c <= 0x115f) ||
137
+ (c >= 0x2e80 && c <= 0xa4cf) || // 含 CJK 标点 0x3000-0x303f
138
+ (c >= 0xac00 && c <= 0xd7a3) ||
139
+ (c >= 0xf900 && c <= 0xfaff) ||
140
+ (c >= 0xfe30 && c <= 0xfe6f) ||
141
+ (c >= 0xff00 && c <= 0xff60) || // 全角形式(含全角标点)
142
+ (c >= 0xffe0 && c <= 0xffe6)
143
+ );
144
+ }
145
+
146
+ /**
147
+ * CJK 标点(,。、;:?!「」『』《》【】… 及全角/半角形式):两侧都不吃空格。
148
+ * 全角区里要排掉字母与数字(`ABC123` 是正文,不是标点)。
149
+ */
150
+ function isCjkPunct(ch: string): boolean {
151
+ const c = ch.codePointAt(0) ?? 0;
152
+ if (c >= 0x3000 && c <= 0x303f) return true; // CJK 符号与标点
153
+ if (c >= 0xfe30 && c <= 0xfe6f) return true; // 竖排/小型形式
154
+ if (c >= 0xff61 && c <= 0xff65) return true; // 半角片假名标点 。「」、・
155
+ if (c < 0xff01 || c > 0xff60) return false; // 全角形式区
156
+ const digit = c >= 0xff10 && c <= 0xff19;
157
+ const upper = c >= 0xff21 && c <= 0xff3a;
158
+ const lower = c >= 0xff41 && c <= 0xff5a;
159
+ return !digit && !upper && !lower;
160
+ }
161
+
162
+ /** 行尾是这些字符时不补空格:开括号 / 开引号 / 反引号 / 破折号省略号一类。 */
163
+ const NO_SPACE_AFTER = new Set(["(", "[", "{", "<", "\"", "'", "`", "…", "—", "–", "·", "$"]);
164
+ /**
165
+ * 行首是这些字符时不补空格:闭括号 / 收尾标点 / 引号 / 路径与扩展名分隔符。
166
+ * 刻意**不含** `-` 与 `_` —— 它们开头的行更可能是 `--verbose` 这类参数或普通正文,
167
+ * 不补空格会和上一行粘成一个词。
168
+ */
169
+ const NO_SPACE_BEFORE = new Set([
170
+ ")",
171
+ "]",
172
+ "}",
173
+ ">",
174
+ ",",
175
+ ".",
176
+ ";",
177
+ ":",
178
+ "!",
179
+ "?",
180
+ "%",
181
+ "\"",
182
+ "'",
183
+ "`",
184
+ "…",
185
+ ]);
186
+
187
+ /**
188
+ * 两行相接处该填什么。目标是读起来像原句:中文折行处不留空格,英文单词之间留一个,
189
+ * 标点按它自己的排版习惯(ASCII 逗号后有空格、闭括号前没有)。
190
+ */
191
+ function joinSeparator(prevEnd: string, nextStart: string): string {
192
+ if (prevEnd === "" || nextStart === "") return "";
193
+ if (isCjkPunct(prevEnd) || isCjkPunct(nextStart)) return "";
194
+ if (isWideChar(prevEnd) && isWideChar(nextStart)) return "";
195
+ if (NO_SPACE_AFTER.has(prevEnd)) return "";
196
+ if (NO_SPACE_BEFORE.has(nextStart)) return "";
197
+ return " ";
198
+ }
199
+
200
+ /** 取字符串末尾一个码点(不切半个代理对)。 */
201
+ function lastChar(text: string): string {
202
+ if (text === "") return "";
203
+ const last = text.charCodeAt(text.length - 1);
204
+ if (last >= 0xdc00 && last <= 0xdfff && text.length >= 2) {
205
+ return text.slice(-2);
206
+ }
207
+ return text.slice(-1);
208
+ }
209
+
210
+ /** 取字符串开头一个码点。 */
211
+ function firstChar(text: string): string {
212
+ if (text === "") return "";
213
+ const first = text.charCodeAt(0);
214
+ if (first >= 0xd800 && first <= 0xdbff && text.length >= 2) {
215
+ return text.slice(0, 2);
216
+ }
217
+ return text.slice(0, 1);
218
+ }
219
+
220
+ /**
221
+ * 剥掉一行的 Markdown 排版噪声。返回 null 表示整行都该丢掉(围栏标记行 / 分隔线)。
222
+ * 顺序:先判整行丢弃,再依次剥引用 → 标题 → 列表标记(`> - item` 这种组合也剥得干净)。
223
+ */
224
+ function stripLineChrome(line: string): string | null {
225
+ if (FENCE_LINE_RE.test(line) || RULE_LINE_RE.test(line)) return null;
226
+ return line
227
+ .replace(BLOCKQUOTE_RE, "")
228
+ .replace(HEADING_RE, "")
229
+ .replace(LIST_BULLET_RE, "")
230
+ .trim();
231
+ }
232
+
233
+ /**
234
+ * 把整段提示词压平成一行紧凑文本(规则见文件头注释)。空提示词 / 全是空白与排版噪声
235
+ * 时返回空串 —— 调用方按「无摘要」处理。
236
+ */
237
+ export function flattenPrompt(prompt: string, maxScanChars: number = MAX_PROMPT_SCAN_CHARS): string {
238
+ if (prompt === "") return "";
239
+ let src = maxScanChars > 0 && prompt.length > maxScanChars ? prompt.slice(0, maxScanChars) : prompt;
240
+ // 扫描上限可能切在代理对中间:丢掉落单的高位代理,免得后面按码点走时出现半个字符。
241
+ const tail = src.charCodeAt(src.length - 1);
242
+ if (tail >= 0xd800 && tail <= 0xdbff) src = src.slice(0, -1);
243
+
244
+ src = src.replace(ANSI_RE, "").replace(ZERO_WIDTH_RE, "").replace(UNICODE_SPACE_RE, " ");
245
+
246
+ let out = "";
247
+ for (const rawLine of src.split(LINE_BREAK_RE)) {
248
+ // 制表符按「一个空格」处理而不是直接删:`a\tb` 删掉会把两个词粘成 `ab`。
249
+ const cleaned = rawLine.replace(/\t/g, " ").replace(CONTROL_RE, "").trim();
250
+ if (cleaned === "") continue; // 空行 / 纯缩进行:不产生额外空格
251
+ const content = stripLineChrome(cleaned);
252
+ if (content === null || content === "") continue;
253
+ out += joinSeparator(lastChar(out), firstChar(content)) + content;
254
+ }
255
+ return out.replace(SPACE_RUN_RE, " ").trim();
256
+ }
257
+
258
+ /** 字素切分:有 `Intl.Segmenter` 就用(代理对 / 组合字符不拆半),没有就按码点。 */
259
+ const graphemeSegmenter =
260
+ typeof Intl !== "undefined" && typeof Intl.Segmenter === "function"
261
+ ? new Intl.Segmenter(undefined, { granularity: "grapheme" })
262
+ : null;
263
+
264
+ function graphemes(text: string): string[] {
265
+ if (graphemeSegmenter !== null) {
266
+ return Array.from(graphemeSegmenter.segment(text), (s) => s.segment);
267
+ }
268
+ return [...text];
269
+ }
270
+
271
+ /**
272
+ * 把文本截到可见宽度不超过 `maxWidth` 的最长前缀(按字素前进,不切半个字符)。
273
+ * 文本本身放得下时原样返回。用 `widthOf(前缀)` 逐段量而不是逐字符宽度求和,
274
+ * 这样组合字符 / 宽字符的边界判断与渲染器一致。
275
+ */
276
+ export function fitToWidth(text: string, maxWidth: number, widthOf: WidthFn): string {
277
+ if (maxWidth <= 0) return "";
278
+ if (widthOf(text) <= maxWidth) return text;
279
+ let out = "";
280
+ for (const segment of graphemes(text)) {
281
+ const next = out + segment;
282
+ if (widthOf(next) > maxWidth) break;
283
+ out = next;
284
+ }
285
+ return out;
286
+ }
287
+
288
+ export interface PromptSummaryLayout {
289
+ /** 截断后的摘要正文(可能带尾部 `…`),不含 `✦ ` 前缀。 */
290
+ summaryText: string;
291
+ /** 左段与 `✦` 之间应补的空格数(≥ minGap,且使整行恰好占满 totalWidth)。 */
292
+ gap: number;
293
+ }
294
+
295
+ export interface SummaryTextWidthOptions {
296
+ /** 左段(spinner 文案)的可见宽度(ANSI 色码不计)。 */
297
+ leftWidth: number;
298
+ /** 整条 working message 的可见宽度预算(已扣除 spinner 与 padding 占位)。 */
299
+ totalWidth: number;
300
+ /** 可见宽度函数(运行时传 pi-tui 的 visibleWidth)。 */
301
+ widthOf: WidthFn;
302
+ /** 摘要标记符,默认 `✦`。 */
303
+ marker?: string;
304
+ /** 左段与摘要之间的最小空隙,默认 1 列。 */
305
+ minGap?: number;
306
+ /**
307
+ * 摘要段(`marker` + 空格 + 正文)的可见宽度上限;缺省不限(只受
308
+ * totalWidth 约束)。
309
+ */
310
+ maxWidth?: number;
311
+ }
312
+
313
+ export interface PromptSummaryOptions extends SummaryTextWidthOptions {
314
+ /** 压平后的提示词(见 `flattenPrompt`);空串直接返回 null。 */
315
+ promptText: string;
316
+ /** 摘要正文的最小可用宽度,低于它整条摘要省略,默认 4 列。 */
317
+ minTextWidth?: number;
318
+ }
319
+
320
+ /**
321
+ * 摘要正文(不含 `✦ ` 前缀)可用的可见列数:预算 − 左段 − 最小空隙 − 标记前缀,
322
+ * 再受 `maxWidth`(整段上限)封顶。返回值 ≤ 0 表示这一格放不下摘要。
323
+ *
324
+ * 渲染(`layoutPromptSummary`)与「要不要请模型摘要 / 请它写多长」(`summary-request.ts`)
325
+ * 都基于它,两处必须得出同一个数 —— 否则模型按 40 列写的摘要会撞上实际只有 20 列的位置。
326
+ */
327
+ export function availableSummaryTextWidth(opts: SummaryTextWidthOptions): number {
328
+ const marker = opts.marker ?? SUMMARY_MARKER;
329
+ const minGap = opts.minGap ?? DEFAULT_MIN_GAP;
330
+ const markerWidth = opts.widthOf(`${marker} `);
331
+ let avail = opts.totalWidth - opts.leftWidth - minGap - markerWidth;
332
+ if (opts.maxWidth !== undefined) {
333
+ avail = Math.min(avail, opts.maxWidth - markerWidth);
334
+ }
335
+ return avail;
336
+ }
337
+
338
+ /**
339
+ * 计算右对齐布局。返回 null 表示放不下(左段太宽 / 终端太窄 / 提示词为空),
340
+ * 调用方应只显示左段。
341
+ *
342
+ * 不变量(非 null 时恒成立,单测逐条断言):
343
+ * leftWidth + gap + widthOf(marker + " ") + widthOf(summaryText) === totalWidth
344
+ * gap ≥ minGap
345
+ */
346
+ export function layoutPromptSummary(opts: PromptSummaryOptions): PromptSummaryLayout | null {
347
+ const marker = opts.marker ?? SUMMARY_MARKER;
348
+ const minGap = opts.minGap ?? DEFAULT_MIN_GAP;
349
+ const minTextWidth = opts.minTextWidth ?? MIN_SUMMARY_TEXT_WIDTH;
350
+ const text = opts.promptText;
351
+ if (text === "") return null;
352
+
353
+ const widthOf = opts.widthOf;
354
+ const markerWidth = widthOf(`${marker} `); // 布局只需它的宽度(可用宽度由 helper 算)
355
+ // 富余(未用满的列数)全部并进 gap:短摘要时它把摘要推到行尾(右对齐),
356
+ // 截断摘要时它恰好等于(或略大于)minGap。
357
+ const avail = availableSummaryTextWidth(opts);
358
+ if (avail < minTextWidth) return null;
359
+
360
+ let summaryText: string;
361
+ if (widthOf(text) <= avail) {
362
+ // 整段放得下:完整展示、不补省略号(用户要求:短提示词全量右对齐)。
363
+ summaryText = text;
364
+ } else {
365
+ // 放不下:给省略号留出宽度后截断;截断点后的行尾空格去掉,避免 "abc …"。
366
+ const budget = avail - widthOf(ELLIPSIS);
367
+ summaryText = `${fitToWidth(text, budget, widthOf).trimEnd()}${ELLIPSIS}`;
368
+ }
369
+
370
+ // gap 吃掉全部余量:短摘要时它把摘要推到行尾(右对齐),
371
+ // 截断摘要时它恰好等于(或略大于)minGap。
372
+ const gap = opts.totalWidth - opts.leftWidth - markerWidth - widthOf(summaryText);
373
+ if (gap < minGap) return null; // 防御:理论上走不到(avail 已保证)
374
+ return { summaryText, gap };
375
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@bachi/pi-coder",
3
+ "version": "1.0.0",
4
+ "description": "A complete Pi coding-agent environment: 22 TUI extensions, 3 themes and the global config files that make them work together.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi",
10
+ "pi-coding-agent",
11
+ "pi-extension",
12
+ "extensions",
13
+ "themes",
14
+ "theme",
15
+ "statusline",
16
+ "tui",
17
+ "terminal",
18
+ "coding-agent",
19
+ "agent"
20
+ ],
21
+ "homepage": "https://github.com/jayli/pi-coder#readme",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/jayli/pi-coder.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/jayli/pi-coder/issues"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "files": [
33
+ "extensions",
34
+ "themes",
35
+ "config",
36
+ "docs",
37
+ "README.md",
38
+ "CHANGELOG.md",
39
+ "LICENSE"
40
+ ],
41
+ "pi": {
42
+ "extensions": [
43
+ "./extensions"
44
+ ],
45
+ "themes": [
46
+ "./themes"
47
+ ]
48
+ },
49
+ "peerDependencies": {
50
+ "@earendil-works/pi-coding-agent": "*",
51
+ "@earendil-works/pi-tui": "*",
52
+ "typebox": "*"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "@earendil-works/pi-coding-agent": {
56
+ "optional": true
57
+ },
58
+ "@earendil-works/pi-tui": {
59
+ "optional": true
60
+ },
61
+ "typebox": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "engines": {
66
+ "node": ">=22.19.0"
67
+ },
68
+ "scripts": {
69
+ "test": "node --test"
70
+ }
71
+ }
@@ -0,0 +1,97 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "ayu",
4
+ "vars": {
5
+ "bg": "#0D1017",
6
+ "panelBg": "#0B0E14",
7
+ "editorLine": "#151C28",
8
+ "fg": "#BFBDB6",
9
+ "muted": "#6B7385",
10
+ "dimmed": "#515868",
11
+ "accent": "#E6B450",
12
+ "orange": "#FF8F40",
13
+ "yellow": "#E6B673",
14
+ "green": "#AAD94C",
15
+ "stringGreen": "#67a567",
16
+ "cyan": "#95E6CB",
17
+ "blue": "#39BAE6",
18
+ "entityBlue": "#59C2FF",
19
+ "purple": "#D2A6FF",
20
+ "red": "#D95757",
21
+ "operator": "#F29668",
22
+ "function": "#FFB454",
23
+ "borderMuted": "#232938",
24
+ "thinkingGrey": "#626262",
25
+ "bashOutput": "#6B7385",
26
+ "selectedBg": "#161D2B",
27
+ "userMessageBg": "#1b1c1d",
28
+ "toolPendingBg": "#171F24",
29
+ "toolSuccessBg": "#10151F",
30
+ "toolErrorBg": "#1B171D",
31
+ "infoBg": "#1A1610",
32
+ "diffAddedBg": "#1d241c",
33
+ "diffRemovedBg": "#321d23"
34
+ },
35
+ "colors": {
36
+ "accent": "accent",
37
+ "border": "dimmed",
38
+ "borderAccent": "accent",
39
+ "borderMuted": "borderMuted",
40
+ "success": "green",
41
+ "error": "red",
42
+ "warning": "orange",
43
+ "muted": "muted",
44
+ "dim": "dimmed",
45
+ "text": "fg",
46
+ "thinkingText": "muted",
47
+ "selectedBg": "selectedBg",
48
+ "userMessageBg": "userMessageBg",
49
+ "userMessageText": "fg",
50
+ "customMessageBg": "userMessageBg",
51
+ "customMessageText": "fg",
52
+ "customMessageLabel": "accent",
53
+ "toolPendingBg": "toolPendingBg",
54
+ "toolSuccessBg": "toolSuccessBg",
55
+ "toolErrorBg": "toolErrorBg",
56
+ "toolTitle": "accent",
57
+ "toolOutput": "fg",
58
+ "bashOutput": "bashOutput",
59
+ "mdHeading": "orange",
60
+ "mdLink": "entityBlue",
61
+ "mdLinkUrl": "muted",
62
+ "mdCode": "cyan",
63
+ "mdCodeBlock": "fg",
64
+ "mdCodeBlockBorder": "dimmed",
65
+ "mdQuote": "muted",
66
+ "mdQuoteBorder": "dimmed",
67
+ "mdHr": "dimmed",
68
+ "mdListBullet": "orange",
69
+ "toolDiffAdded": "green",
70
+ "toolDiffRemoved": "red",
71
+ "toolDiffContext": "muted",
72
+ "toolDiffAddedBg": "diffAddedBg",
73
+ "toolDiffRemovedBg": "diffRemovedBg",
74
+ "syntaxComment": "muted",
75
+ "syntaxKeyword": "orange",
76
+ "syntaxFunction": "function",
77
+ "syntaxVariable": "fg",
78
+ "syntaxString": "stringGreen",
79
+ "syntaxNumber": "purple",
80
+ "syntaxType": "entityBlue",
81
+ "syntaxOperator": "operator",
82
+ "syntaxPunctuation": "muted",
83
+ "thinkingOff": "dimmed",
84
+ "thinkingMinimal": "muted",
85
+ "thinkingLow": "blue",
86
+ "thinkingMedium": "accent",
87
+ "thinkingHigh": "orange",
88
+ "thinkingXhigh": "thinkingGrey",
89
+ "thinkingMax": "thinkingGrey",
90
+ "bashMode": "orange"
91
+ },
92
+ "export": {
93
+ "pageBg": "panelBg",
94
+ "cardBg": "bg",
95
+ "infoBg": "infoBg"
96
+ }
97
+ }
@@ -0,0 +1,103 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "catppuccin",
4
+ "vars": {
5
+ "rosewater": "#F5E0DC",
6
+ "flamingo": "#F2CDCD",
7
+ "pink": "#F5C2E7",
8
+ "mauve": "#CBA6F7",
9
+ "red": "#F38BA8",
10
+ "maroon": "#EBA0AC",
11
+ "peach": "#FAB387",
12
+ "yellow": "#F9E2AF",
13
+ "green": "#A6E3A1",
14
+ "teal": "#94E2D5",
15
+ "sky": "#89DCEB",
16
+ "sapphire": "#74C7EC",
17
+ "blue": "#89B4FA",
18
+ "lavender": "#B4BEFE",
19
+ "text": "#CDD6F4",
20
+ "subtext1": "#BAC2DE",
21
+ "subtext0": "#A6ADC8",
22
+ "overlay2": "#9399B2",
23
+ "overlay1": "#7F849C",
24
+ "overlay0": "#6C7086",
25
+ "surface2": "#585B70",
26
+ "surface1": "#45475A",
27
+ "surface0": "#313244",
28
+ "base": "#1E1E2E",
29
+ "mantle": "#181825",
30
+ "crust": "#11111B",
31
+ "macchiatoLavender": "#B7BDF8",
32
+ "exportInfoTint": "#242031",
33
+ "dimText": "#7e8398",
34
+ "secondaryText": "#82889e",
35
+ "selected": "#1e1e29",
36
+ "successPanel": "#151515",
37
+ "errorPanel": "#171010",
38
+ "pendingPanel": "#0b151f",
39
+ "diffAddedBg": "#1c241b",
40
+ "diffRemovedBg": "#2e1c21"
41
+ },
42
+ "colors": {
43
+ "accent": "macchiatoLavender",
44
+ "border": "surface1",
45
+ "borderAccent": "mauve",
46
+ "borderMuted": "surface0",
47
+ "success": "green",
48
+ "error": "maroon",
49
+ "warning": "peach",
50
+ "muted": "secondaryText",
51
+ "dim": "dimText",
52
+ "text": "",
53
+ "thinkingText": "secondaryText",
54
+ "selectedBg": "selected",
55
+ "userMessageBg": "mantle",
56
+ "userMessageText": "text",
57
+ "customMessageBg": "mantle",
58
+ "customMessageText": "text",
59
+ "customMessageLabel": "mauve",
60
+ "toolPendingBg": "pendingPanel",
61
+ "toolSuccessBg": "successPanel",
62
+ "toolErrorBg": "errorPanel",
63
+ "toolTitle": "mauve",
64
+ "toolOutput": "secondaryText",
65
+ "mdHeading": "peach",
66
+ "mdLink": "blue",
67
+ "mdLinkUrl": "subtext1",
68
+ "mdCode": "teal",
69
+ "mdCodeBlock": "text",
70
+ "mdCodeBlockBorder": "surface1",
71
+ "mdQuote": "subtext1",
72
+ "mdQuoteBorder": "surface1",
73
+ "mdHr": "overlay0",
74
+ "mdListBullet": "peach",
75
+ "toolDiffAdded": "green",
76
+ "toolDiffRemoved": "red",
77
+ "toolDiffContext": "subtext1",
78
+ "toolDiffAddedBg": "diffAddedBg",
79
+ "toolDiffRemovedBg": "diffRemovedBg",
80
+ "syntaxComment": "subtext0",
81
+ "syntaxKeyword": "mauve",
82
+ "syntaxFunction": "sky",
83
+ "syntaxVariable": "",
84
+ "syntaxString": "green",
85
+ "syntaxNumber": "peach",
86
+ "syntaxType": "yellow",
87
+ "syntaxOperator": "sapphire",
88
+ "syntaxPunctuation": "overlay1",
89
+ "thinkingOff": "overlay1",
90
+ "thinkingMinimal": "overlay2",
91
+ "thinkingLow": "blue",
92
+ "thinkingMedium": "mauve",
93
+ "thinkingHigh": "peach",
94
+ "thinkingXhigh": "blue",
95
+ "thinkingMax": "blue",
96
+ "bashMode": "peach"
97
+ },
98
+ "export": {
99
+ "pageBg": "crust",
100
+ "cardBg": "mantle",
101
+ "infoBg": "exportInfoTint"
102
+ }
103
+ }