pi-firecode 1.0.0 → 1.0.2

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 (93) hide show
  1. package/README.md +59 -41
  2. package/README.zh-CN.md +78 -0
  3. package/config.example.jsonc +21 -22
  4. package/dist/config.example.jsonc +90 -0
  5. package/dist/index.js +11443 -0
  6. package/dist/master/prompts/master.en.md +22 -0
  7. package/{master → dist/master}/prompts/master.zh.md +3 -3
  8. package/dist/master/prompts/worker.en.md +1 -0
  9. package/dist/watcher/prompts/watch.en.md +43 -0
  10. package/package.json +10 -27
  11. package/activity.ts +0 -92
  12. package/busy.ts +0 -188
  13. package/config.ts +0 -500
  14. package/deliver.ts +0 -119
  15. package/flame.ts +0 -149
  16. package/format.ts +0 -133
  17. package/header.ts +0 -218
  18. package/herdr-client.ts +0 -60
  19. package/index.ts +0 -68
  20. package/jsonc.ts +0 -34
  21. package/master/actions.ts +0 -293
  22. package/master/activity-list.ts +0 -281
  23. package/master/event-card.ts +0 -87
  24. package/master/event-format.ts +0 -83
  25. package/master/guard.ts +0 -46
  26. package/master/index.ts +0 -252
  27. package/master/list-view.ts +0 -129
  28. package/master/outbox.ts +0 -188
  29. package/master/prompt.ts +0 -26
  30. package/master/role.ts +0 -18
  31. package/master/run.ts +0 -315
  32. package/master/runtime.ts +0 -259
  33. package/master/spawn.ts +0 -234
  34. package/master/state.ts +0 -225
  35. package/master/worker-view.ts +0 -517
  36. package/provider/claude-sub.ts +0 -151
  37. package/provider/openai-native/index.ts +0 -8
  38. package/provider/openai-native/src/compact-client.ts +0 -350
  39. package/provider/openai-native/src/config.ts +0 -297
  40. package/provider/openai-native/src/extension.ts +0 -93
  41. package/provider/openai-native/src/native-compaction.ts +0 -151
  42. package/provider/openai-native/src/native-details.ts +0 -157
  43. package/provider/openai-native/src/native-replay.ts +0 -253
  44. package/provider/openai-native/src/native-runtime.ts +0 -144
  45. package/provider/openai-native/src/options.ts +0 -87
  46. package/provider/openai-native/src/request-pipeline.ts +0 -21
  47. package/provider/openai-native/src/responses-input.ts +0 -433
  48. package/review/advisor.ts +0 -110
  49. package/review/card.ts +0 -398
  50. package/review/checkpoint.ts +0 -378
  51. package/review/evidence.ts +0 -230
  52. package/review/index.ts +0 -992
  53. package/review/occupancy.ts +0 -25
  54. package/review/outcome.ts +0 -98
  55. package/review/prompt.ts +0 -209
  56. package/review/reviewer.ts +0 -353
  57. package/review/session.ts +0 -110
  58. package/review/state.ts +0 -877
  59. package/review/ui.ts +0 -89
  60. package/round-recorder.ts +0 -18
  61. package/session/herdr-display.ts +0 -87
  62. package/session/presets.ts +0 -300
  63. package/session/quota.ts +0 -123
  64. package/session/rename.ts +0 -46
  65. package/session/stats.ts +0 -313
  66. package/statusbar/index.ts +0 -256
  67. package/statusbar/render.ts +0 -113
  68. package/theme.ts +0 -45
  69. package/tools/actions.ts +0 -53
  70. package/tools/assistant-view.ts +0 -64
  71. package/tools/click-anchor.ts +0 -56
  72. package/tools/group-view.ts +0 -420
  73. package/tools/grouping.ts +0 -158
  74. package/tools/host.ts +0 -369
  75. package/tools/index.ts +0 -186
  76. package/tools/line.ts +0 -231
  77. package/tools/machine.ts +0 -78
  78. package/tools/parts.ts +0 -143
  79. package/tools/round.ts +0 -95
  80. package/tools/timing.ts +0 -22
  81. package/tools/turn-clock.ts +0 -41
  82. package/tools/turn-summary.ts +0 -109
  83. package/truncated-write.ts +0 -18
  84. package/watcher/card.ts +0 -91
  85. package/watcher/index.ts +0 -176
  86. package/watcher/observer.ts +0 -75
  87. package/watcher/transcript.ts +0 -56
  88. /package/{master → dist/master}/prompts/worker.zh.md +0 -0
  89. /package/{review → dist/review}/prompts/advisor.en.md +0 -0
  90. /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
  91. /package/{review → dist/review}/prompts/review.en.md +0 -0
  92. /package/{review → dist/review}/prompts/review.zh.md +0 -0
  93. /package/{watcher → dist/watcher}/prompts/watch.zh.md +0 -0
package/flame.ts DELETED
@@ -1,149 +0,0 @@
1
- /**
2
- * 火焰动效:全局唯一动画时钟、盲文火苗与落定过渡。凡是“正在运行”的东西都用这里的火苗;
3
- * 所有动效由同一个时钟驱动,没有订阅者时计时器停止,帧号取绝对时间,多处火苗天然同步。
4
- */
5
- export type Rgb = readonly [number, number, number];
6
-
7
- /** 火焰色板:heat 从余烬烧到白金;ash 是横幅副标题淡入的终色灰。 */
8
- export const HEAT_COLORS = {
9
- ember: [110, 30, 22],
10
- red: [255, 47, 32],
11
- orange: [255, 119, 28],
12
- gold: [255, 195, 61],
13
- white: [255, 239, 184],
14
- ash: [128, 123, 115],
15
- green: [124, 196, 120],
16
- fail: [226, 78, 66],
17
- } as const satisfies Record<string, Rgb>;
18
-
19
- const FPS = 12;
20
- const FRAME_MS = 1000 / FPS;
21
- const SETTLE_MS = 400;
22
- /** 一格火苗与三格火苗的火舌高度轮廓(每格两个点列)。 */
23
- const PROFILE = { 1: [0.75, 1], 3: [0.3, 0.6, 0.9, 1, 0.7, 0.35] } as const;
24
- // 盲文点位:左列自上而下 1 2 3 7,右列 4 5 6 8。
25
- const DOT = [
26
- [0x01, 0x02, 0x04, 0x40],
27
- [0x08, 0x10, 0x20, 0x80],
28
- ] as const;
29
- const HEAT_STOPS: readonly [number, Rgb][] = [
30
- [0, HEAT_COLORS.ember],
31
- [0.3, HEAT_COLORS.red],
32
- [0.55, HEAT_COLORS.orange],
33
- [0.8, HEAT_COLORS.gold],
34
- [1, HEAT_COLORS.white],
35
- ];
36
-
37
- const listeners = new Set<() => void>();
38
- let timer: ReturnType<typeof setInterval> | undefined;
39
-
40
- /** 订阅动画帧;返回取消函数。最后一个订阅者离开时计时器随之停止。 */
41
- export function onFrame(listener: () => void): () => void {
42
- listeners.add(listener);
43
- if (!timer) {
44
- timer = setInterval(() => {
45
- for (const notify of listeners) notify();
46
- }, FRAME_MS);
47
- timer.unref?.();
48
- }
49
- return () => {
50
- listeners.delete(listener);
51
- if (listeners.size || !timer) return;
52
- clearInterval(timer);
53
- timer = undefined;
54
- };
55
- }
56
-
57
- /** 按帧量化的绝对秒数;所有火苗共用同一相位基准。 */
58
- const frameSeconds = () => Math.floor(Date.now() / FRAME_MS) / FPS;
59
-
60
- const clamp = (value: number) => Math.min(1, Math.max(0, value));
61
- const easeOut = (k: number) => 1 - (1 - clamp(k)) ** 3;
62
-
63
- export function mix(from: Rgb, to: Rgb, k: number): Rgb {
64
- const t = clamp(k);
65
- return [from[0] + (to[0] - from[0]) * t, from[1] + (to[1] - from[1]) * t, from[2] + (to[2] - from[2]) * t];
66
- }
67
-
68
- export function heat(value: number): Rgb {
69
- const x = clamp(value);
70
- for (let index = 1; index < HEAT_STOPS.length; index++) {
71
- const [at, color] = HEAT_STOPS[index];
72
- const [fromAt, from] = HEAT_STOPS[index - 1];
73
- if (x <= at) return mix(from, color, (x - fromAt) / (at - fromAt));
74
- }
75
- return HEAT_COLORS.white;
76
- }
77
-
78
- /** 只设置/复位前景色,不注入全量 reset,可安全嵌进带背景的行。 */
79
- export function paint(color: Rgb, text: string): string {
80
- return text ? `\x1b[38;2;${color[0] | 0};${color[1] | 0};${color[2] | 0}m${text}\x1b[39m` : "";
81
- }
82
-
83
- /** 三个不可公约频率叠加的火势,0.1–1 之间起伏。 */
84
- export function flicker(phase: number, t = frameSeconds()): number {
85
- const tau = Math.PI * 2;
86
- return 0.55
87
- + 0.2 * Math.sin(tau * (1.1 * t + phase))
88
- + 0.15 * Math.sin(tau * (2.7 * t + 1.7 * phase) + 1.3)
89
- + 0.1 * Math.sin(tau * (5.9 * t + 3.1 * phase) + 0.4);
90
- }
91
-
92
- /**
93
- * 盲文火苗:每个点列是一根随时钟起伏的火舌,火尖随风摆,顶行只留一个点保持尖顶。
94
- * phase 让并列的火苗错开。
95
- */
96
- export function flame(cells: 1 | 3, phase: number): string {
97
- const profile = PROFILE[cells];
98
- const columns = cells * 2;
99
- const t = frameSeconds();
100
- const sway = Math.round(Math.sin(Math.PI * 2 * (0.7 * t + phase)) * 0.6);
101
- const heights = Array.from({ length: columns }, (_, index) => {
102
- const base = profile[Math.min(columns - 1, Math.max(0, index - sway))];
103
- return Math.round(clamp(base * (0.7 + 0.45 * flicker(phase + index * 0.37, t))) * 4);
104
- });
105
- const peak = Math.max(...heights);
106
- let topSeen = false;
107
- let out = "";
108
- for (let cell = 0; cell < cells; cell++) {
109
- let bits = 0;
110
- for (let side = 0; side < 2; side++) {
111
- let height = heights[cell * 2 + side];
112
- if (height === peak && height > 1) {
113
- if (topSeen) height -= 1;
114
- topSeen = true;
115
- }
116
- for (let row = 0; row < height; row++) bits |= DOT[side][3 - row];
117
- }
118
- const core = cells === 1 ? 0.5 : 1 - Math.abs(cell - (cells - 1) / 2) / cells;
119
- const color = heat(0.35 + 0.45 * core + 0.2 * flicker(phase + cell, t));
120
- out += paint(color, String.fromCodePoint(0x2800 + bits));
121
- }
122
- return out;
123
- }
124
-
125
- export type Settle = "done" | "failed";
126
-
127
- /**
128
- * 落定标记:歇下那一刻就是 ✓/✗(与定格文字同帧出现,不留冷却中的火苗残帧),颜色在 0.4 秒内从火焰橙转到终色。
129
- */
130
- export function settleMark(kind: Settle, sinceMs: number): string {
131
- const k = easeOut(sinceMs / SETTLE_MS);
132
- return kind === "done"
133
- ? paint(mix(HEAT_COLORS.orange, HEAT_COLORS.green, k), "✓")
134
- : paint(mix(HEAT_COLORS.orange, HEAT_COLORS.fail, k), "✗");
135
- }
136
-
137
- /** 落定过渡是否仍在播放;调用方据此决定是否继续订阅时钟。 */
138
- export const settling = (sinceMs: number) => sinceMs < SETTLE_MS;
139
-
140
- const SPINNER = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
141
-
142
- /** 审查:金色盲文转圈点,与火苗同一时钟、同一字符族,靠金色与火焰橙区分;phase 错开并列的转圈。 */
143
- export function reviewMark(phase = 0): string {
144
- const frame = Math.floor(Date.now() / FRAME_MS + phase * SPINNER.length);
145
- return paint(HEAT_COLORS.gold, SPINNER[frame % SPINNER.length]);
146
- }
147
-
148
- /** 并列火苗的相位:按序号错开,避免一排火苗同起同落。 */
149
- export const phaseOf = (index: number) => index * 0.23 + 0.11;
package/format.ts DELETED
@@ -1,133 +0,0 @@
1
- /** 宽度、文本与数值格式化:状态栏与工具行共用。 */
2
- import { visibleWidth } from "@earendil-works/pi-tui";
3
-
4
- const ELLIPSIS = "…";
5
- const ANSI_SEQUENCE = /(\x1b(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:\x07|\x1b\\)))/g;
6
- const segmenter = new Intl.Segmenter(undefined, { granularity: "grapheme" });
7
-
8
- /** 压平换行与连续空白,用于把任意文本塞进单行 UI。 */
9
- /** 消息 content 里的正文:字符串原样;块数组只取 text 块,按行拼接。全插件只此一处。 */
10
- export function textOf(content: unknown): string {
11
- if (typeof content === "string") return content;
12
- if (!Array.isArray(content)) return "";
13
- return content
14
- .filter((part): part is { type: "text"; text: string } =>
15
- typeof part === "object" && part !== null && (part as { type?: unknown }).type === "text"
16
- && typeof (part as { text?: unknown }).text === "string")
17
- .map((part) => part.text)
18
- .join("\n");
19
- }
20
-
21
- export function oneLine(value = ""): string {
22
- return value
23
- .replace(/[\r\n\t]+/g, " ")
24
- .replace(/ {2,}/g, " ")
25
- .trim();
26
- }
27
-
28
- export type ClipSide = "start" | "end";
29
-
30
- /**
31
- * 按显示宽度截断,保留完整字素簇。
32
- * `from: "end"` 保留头部(命令),`from: "start"` 保留尾部(路径 basename)。
33
- */
34
- export function clip(
35
- text: string,
36
- width: number,
37
- from: ClipSide = "end",
38
- ellipsis: string = ELLIPSIS,
39
- ): string {
40
- if (width <= 0) return "";
41
- const textWidth = visibleWidth(text);
42
- if (textWidth <= width) return text;
43
- const ellipsisWidth = visibleWidth(ellipsis);
44
- if (ellipsisWidth > width) return clip(ellipsis, width, "end", "");
45
- const target = width - ellipsisWidth;
46
- let output = from === "start" ? ellipsis : "";
47
- let column = 0;
48
- let clipped = false;
49
- const chunks = text.split(ANSI_SEQUENCE);
50
- for (let index = 0; index < chunks.length; index++) {
51
- if (index % 2) {
52
- // 连被裁掉文字后的颜色关闭/链接关闭也保留,避免样式泄漏;不注入全量 reset。
53
- output += chunks[index];
54
- continue;
55
- }
56
- for (const { segment } of segmenter.segment(chunks[index])) {
57
- const columns = visibleWidth(segment);
58
- if (from === "start") {
59
- if (column >= textWidth - target) output += segment;
60
- } else if (!clipped) {
61
- if (column + columns <= target) output += segment;
62
- else { output += ellipsis; clipped = true; }
63
- }
64
- column += columns;
65
- }
66
- }
67
- return output;
68
- }
69
-
70
- /** 1234 → 1.2k,1_500_000 → 1.5M,整数不带小数(1M、200k);0 与 undefined 显示为 ?。 */
71
- export function formatTokens(tokens: number): string {
72
- if (tokens >= 1_000_000) return `${scaled(tokens / 1_000_000)}M`;
73
- if (tokens >= 1_000) return `${scaled(tokens / 1_000)}k`;
74
- return tokens ? `${tokens}` : "?";
75
- }
76
-
77
- const scaled = (value: number) => (value >= 10 ? Math.round(value) : Number(value.toFixed(1)));
78
-
79
- const FENCE = /^\s*```/u;
80
- const HEADING = /^\s*#{1,6}\s+/u;
81
- const TABLE_ROW = /^\s*\|.*\|\s*$/u;
82
- const RULE = /^\s*([-*_])(?:\s*\1){2,}\s*$/u;
83
- /** 块级标记:引用符、列表符、序号(含任务框)。 */
84
- const BLOCK_MARK = /^\s*(?:>\s*)*(?:(?:[-*+]|\d+[.)])\s+(?:\[[ xX]\]\s+)?)?/u;
85
- /** 句末:中文句末标点,或后接空白/行尾的英文句末标点——但缩写的点不算。 */
86
- const SENTENCE_END = /[。!?]|(?<!\b(?:e\.g|i\.e|etc|vs|cf|Mr|Mrs|Ms|Dr|St|No))[.!?](?=\s|$)/u;
87
-
88
- /**
89
- * 一行预览用的首句(机器消息行、中间回复、Master 事件卡与会话标题共用):认 Markdown 结构,
90
- * 跳过标题、围栏、表格行与分隔线,剥掉引用符、列表符与序号,去掉行内标记;只有标题没有正文时才用标题文字。
91
- * 首句以冒号结尾(如“标准输出:”)本身没有信息,并入下一有效行的首句。
92
- */
93
- export function firstSentence(text: string): string {
94
- const lines = text.split("\n").filter((line) => !FENCE.test(line) && !TABLE_ROW.test(line) && !RULE.test(line));
95
- const body = lines.filter((line) => !HEADING.test(line));
96
- const source = body.some((line) => line.trim()) ? body : lines.map((line) => line.replace(HEADING, ""));
97
- return sentenceOf(source.map((line) => oneLine(inline(line.replace(BLOCK_MARK, "")))).filter(Boolean));
98
- }
99
-
100
- /** 预览是纯文本:链接只留文字,去掉粗体与行内代码标记。 */
101
- function inline(line: string): string {
102
- return line
103
- .replace(/!?\[([^\]]*)\]\([^)]*\)/gu, "$1")
104
- .replace(/(\*\*|__)(.+?)\1/gu, "$2")
105
- .replace(/`([^`]+)`/gu, "$1");
106
- }
107
-
108
- function sentenceOf([head = "", ...rest]: string[]): string {
109
- const end = SENTENCE_END.exec(head);
110
- const sentence = end ? head.slice(0, end.index + 1) : head;
111
- const colon = /[::]$/u.exec(sentence)?.[0];
112
- if (!colon || rest.length === 0) return sentence;
113
- return `${sentence}${colon === ":" ? " " : ""}${sentenceOf(rest)}`;
114
- }
115
-
116
- /** 紧凑耗时:十秒以内保留一位小数,长耗时按小时、分钟、秒进位。 */
117
- export function formatDuration(milliseconds: number): string {
118
- if (milliseconds < 10_000) return `${(milliseconds / 1_000).toFixed(1)}s`;
119
- const totalSeconds = Math.round(milliseconds / 1_000);
120
- if (totalSeconds < 60) return `${totalSeconds}s`;
121
- const hours = Math.floor(totalSeconds / 3_600);
122
- const minutes = Math.floor(totalSeconds / 60) % 60;
123
- const seconds = totalSeconds % 60;
124
- return `${hours ? `${hours}h` : ""}${minutes ? `${minutes}m` : ""}${seconds ? `${seconds}s` : ""}`;
125
- }
126
-
127
- /** 去掉模型 id 的 provider 前缀与日期后缀。 */
128
- export function formatModelName(id: string | undefined): string {
129
- if (!id) return "no-model";
130
- return (id.split("/").pop() ?? id)
131
- .replace(/-\d{8}$/, "")
132
- .replace(/-\d{4}-\d{2}-\d{2}$/, "");
133
- }
package/header.ts DELETED
@@ -1,218 +0,0 @@
1
- /**
2
- * 会话启动横幅:半格方块像素火焰 + 字标,按终端宽度分三档,三档都带同一行副标题(pi 版本 · 工作目录,
3
- * 放不下只留目录、从开头按整段省略)。启动时字标自左向右点亮、扫光一道,约 1.5 秒后定格并退订动画时钟——
4
- * 横幅会滚出视口,定格后不再触发重绘。
5
- */
6
- import { homedir } from "node:os";
7
- import { type ExtensionAPI, VERSION } from "@earendil-works/pi-coding-agent";
8
- import type { TUI } from "@earendil-works/pi-tui";
9
- import { visibleWidth } from "@earendil-works/pi-tui";
10
- import { flame, flicker, HEAT_COLORS, heat, mix, onFrame, paint, type Rgb } from "./flame.js";
11
- import { clip } from "./format.js";
12
-
13
- const LARGE_MIN_WIDTH = 83;
14
- const MID_MIN_WIDTH = 52;
15
- const REVEAL_END = 0.7;
16
- const SWEEP_END = 1.5;
17
- const TINY_FLAME_PHASE = 0.4;
18
- /** 副标题从这一深底色淡入成灰;终端底色不可知,取深色主题的近似底色。 */
19
- const FADE_FROM: Rgb = [20, 19, 18];
20
- const RULE: Rgb = [60, 57, 53];
21
- /** 单行档副标题至少留这么宽才显示(省略号加几个字),右侧横线至少留这么长。 */
22
- const TINY_SUBTITLE_MIN = 8;
23
- const TINY_RULE_MIN = 3;
24
-
25
- const clamp = (value: number) => Math.min(1, Math.max(0, value));
26
- const ease = (k: number) => 1 - (1 - clamp(k)) ** 3;
27
-
28
- /** 像素画:每个终端格承载上下两个方形像素,▀ 的前景画上半、背景画下半。 */
29
- type Pixels = (Rgb | undefined)[][];
30
-
31
- const background = (color: Rgb) => `\x1b[48;2;${color[0] | 0};${color[1] | 0};${color[2] | 0}m`;
32
-
33
- function halfBlocks(pixels: Pixels): string[] {
34
- const lines: string[] = [];
35
- for (let y = 0; y < pixels.length; y += 2) {
36
- let line = "";
37
- pixels[y].forEach((top, x) => {
38
- const bottom = pixels[y + 1]?.[x];
39
- if (top && bottom) line += `${background(bottom)}${paint(top, "▀")}\x1b[49m`;
40
- else if (top) line += paint(top, "▀");
41
- else if (bottom) line += paint(bottom, "▄");
42
- else line += " ";
43
- });
44
- lines.push(line);
45
- }
46
- return lines;
47
- }
48
-
49
- /** 水滴火焰:下部半圆、上部按幂曲线收成火尖,火尖随时间摆动;离焰芯越近越接近白金。 */
50
- function roundFlame(widthPx: number, heightPx: number, t: number): Pixels {
51
- const radius = widthPx / 2;
52
- const belly = 0.36;
53
- const tall = 0.9 + 0.1 * flicker(0.2, t);
54
- return Array.from({ length: heightPx }, (_, y) => {
55
- const row: (Rgb | undefined)[] = Array(widthPx).fill(undefined);
56
- const v = (heightPx - 0.5 - y) / heightPx / tall;
57
- if (v > 1) return row;
58
- const half = v < belly
59
- ? radius * Math.sqrt(1 - ((belly - v) / belly) ** 2)
60
- : radius * ((1 - v) / (1 - belly)) ** 1.25;
61
- const sway = 1.3 * v * v * Math.sin(Math.PI * 2 * 0.6 * t) + 0.4 * v * Math.sin(Math.PI * 2 * (1.7 * t + v * 2));
62
- const reach = half + 0.25 * flicker(v * 3.1, t) * v;
63
- for (let x = 0; x < widthPx; x++) {
64
- const distance = Math.abs(x + 0.5 - radius - sway);
65
- if (distance > reach) continue;
66
- const core = 1 - distance / (reach + 0.01);
67
- row[x] = heat(0.12 + 0.55 * core * (1 - v * 0.6) + 0.45 * (1 - v) ** 2);
68
- }
69
- return row;
70
- });
71
- }
72
-
73
- /** 8 像素高的细笔画字形:1 像素笔画,转角缺一角做出圆角。 */
74
- const FONT: Record<string, string[]> = {
75
- F: ["#####", "#....", "#....", "####.", "#....", "#....", "#....", "#...."],
76
- i: ["#", ".", "#", "#", "#", "#", "#", "#"],
77
- r: ["....", "....", ".###", "#...", "#...", "#...", "#...", "#..."],
78
- e: [".....", ".....", ".###.", "#...#", "#####", "#....", "#....", ".####"],
79
- C: [".####", "#....", "#....", "#....", "#....", "#....", "#....", ".####"],
80
- o: [".....", ".....", ".###.", "#...#", "#...#", "#...#", "#...#", ".###."],
81
- d: ["....#", "....#", ".####", "#...#", "#...#", "#...#", "#...#", ".####"],
82
- };
83
- const WORD = "FireCode";
84
-
85
- const wordBits = (gap: number): boolean[][] =>
86
- FONT.F.map((_, y) => [...WORD].flatMap((char, index) => [
87
- ...Array<boolean>(index ? gap : 0).fill(false),
88
- ...[...FONT[char][y]].map((pixel) => pixel === "#"),
89
- ]));
90
- const WORD_BITS = { 1: wordBits(1), 2: wordBits(2) } as const;
91
-
92
- /** 字标:横向红→橙→金;入场时自左向右点亮(亮边白热),随后一道斜向扫光。 */
93
- function wordmark(gap: 1 | 2, t: number): Pixels {
94
- const bits = WORD_BITS[gap];
95
- const width = bits[0].length;
96
- const edge = (t / REVEAL_END) * (width + 8);
97
- const sweep = ((t - REVEAL_END - 0.1) / (SWEEP_END - REVEAL_END)) * (width + 16) - 8;
98
- return bits.map((row, y) => row.map((on, x) => {
99
- if (!on || x > edge) return undefined;
100
- const half = width / 2;
101
- const base = x < half
102
- ? mix(HEAT_COLORS.red, HEAT_COLORS.orange, x / half)
103
- : mix(HEAT_COLORS.orange, HEAT_COLORS.gold, (x - half) / half);
104
- const hotEdge = t < REVEAL_END ? clamp(1 - (edge - x) / 6) : 0;
105
- const shine = t > REVEAL_END && t < SWEEP_END ? clamp(1 - Math.abs(x - y * 0.6 - sweep) / 4.5) : 0;
106
- return mix(base, HEAT_COLORS.white, Math.max(hotEdge, shine * 0.9));
107
- }));
108
- }
109
-
110
- function sideBySide(left: string[], right: string[], gap: number, rightTop = 0): string[] {
111
- const leftWidth = Math.max(...left.map(visibleWidth));
112
- return Array.from({ length: Math.max(left.length, right.length + rightTop) }, (_, index) => {
113
- const cell = left[index] ?? "";
114
- return `${cell}${" ".repeat(leftWidth - visibleWidth(cell) + gap)}${right[index - rightTop] ?? ""}`;
115
- });
116
- }
117
-
118
- /** 副标题的来源:三档共用同一份。 */
119
- interface Subtitle {
120
- version: string;
121
- cwd: string;
122
- }
123
-
124
- /** 放得下就是“pi 版本 · 工作目录”;放不下只留目录,从开头按整段省略(…/末尾几段),连末段都放不下才裁字。 */
125
- function fitSubtitle({ version, cwd }: Subtitle, width: number): string {
126
- const full = `pi ${version} · ${cwd}`;
127
- if (visibleWidth(full) <= width) return full;
128
- const segments = cwd.split("/");
129
- for (let index = 1; index < segments.length; index++) {
130
- const tail = `…/${segments.slice(index).join("/")}`;
131
- if (visibleWidth(tail) <= width) return tail;
132
- }
133
- return clip(segments.at(-1) ?? cwd, width, "start");
134
- }
135
-
136
- /** 副标题:从深底色淡入成灰。 */
137
- function subtitleLine(subtitle: Subtitle, width: number, t: number): string {
138
- return paint(mix(FADE_FROM, HEAT_COLORS.ash, ease((t - 0.4) / 0.6)), fitSubtitle(subtitle, width));
139
- }
140
-
141
- function large(t: number, subtitle: Subtitle): string[] {
142
- const word = halfBlocks(wordmark(2, t));
143
- return sideBySide(halfBlocks(roundFlame(10, 16, t)), [...word, "", subtitleLine(subtitle, visibleWidth(word[0]), t)], 3, 2);
144
- }
145
-
146
- function mid(t: number, subtitle: Subtitle): string[] {
147
- const word = halfBlocks(wordmark(1, t));
148
- return sideBySide(halfBlocks(roundFlame(6, 8, t)), [...word, subtitleLine(subtitle, visibleWidth(word[0]), t)], 2);
149
- }
150
-
151
- /** 一行档:火苗 + 渐变字标逐字亮起 + 副标题 + 向右渐隐的横线;太窄时只留横线。 */
152
- function tiny(width: number, t: number, glyph: string, subtitle: Subtitle): string {
153
- const sweep = ((t - REVEAL_END) / (SWEEP_END - REVEAL_END)) * 14 - 3;
154
- const letters = [...WORD].map((char, index) => {
155
- if (t <= (index / WORD.length) * REVEAL_END) return " ";
156
- const shine = t > REVEAL_END && t < SWEEP_END ? clamp(1 - Math.abs(index - sweep) / 2) : 0;
157
- return paint(mix(mix(HEAT_COLORS.orange, HEAT_COLORS.gold, index / (WORD.length - 1)), HEAT_COLORS.white, shine), char);
158
- }).join("");
159
- const word = `${glyph} \x1b[1m${letters}\x1b[22m `;
160
- const room = width - visibleWidth(word) - 1 - TINY_RULE_MIN;
161
- const head = room >= TINY_SUBTITLE_MIN ? `${word}${subtitleLine(subtitle, room, t)} ` : word;
162
- const length = width - visibleWidth(head);
163
- const reveal = clamp((t - 0.2) / REVEAL_END);
164
- let rule = "";
165
- for (let index = 0; index < length; index++)
166
- rule += index / length <= reveal
167
- ? paint(mix(HEAT_COLORS.gold, RULE, ease(index / Math.max(8, length * 0.7))), "─")
168
- : " ";
169
- return clip(head + rule, width, "end", "");
170
- }
171
-
172
- function center(lines: string[], width: number): string[] {
173
- const pad = " ".repeat(Math.max(0, Math.floor((width - Math.max(...lines.map(visibleWidth))) / 2)));
174
- return lines.map((line) => clip(pad + line, width, "end", ""));
175
- }
176
-
177
- function banner(width: number, t: number, glyph: string, subtitle: Subtitle): string[] {
178
- if (width >= LARGE_MIN_WIDTH) return ["", ...center(large(t, subtitle), width), ""];
179
- if (width >= MID_MIN_WIDTH) return ["", ...center(mid(t, subtitle), width), ""];
180
- return [tiny(width, t, glyph, subtitle), ""];
181
- }
182
-
183
- function createBanner(tui: TUI, subtitle: Subtitle) {
184
- const start = Date.now();
185
- const elapsed = () => Math.min(SWEEP_END, (Date.now() - start) / 1000);
186
- let unsubscribe: (() => void) | undefined;
187
- const dispose = () => {
188
- unsubscribe?.();
189
- unsubscribe = undefined;
190
- };
191
- unsubscribe = onFrame(() => {
192
- tui.requestRender();
193
- if (elapsed() >= SWEEP_END) dispose();
194
- });
195
- let frozenGlyph: string | undefined;
196
- let frozen: { width: number; lines: string[] } | undefined;
197
- return {
198
- invalidate() {},
199
- dispose,
200
- render(width: number): string[] {
201
- const t = elapsed();
202
- if (t < SWEEP_END) return banner(width, t, flame(3, TINY_FLAME_PHASE), subtitle);
203
- if (frozen?.width !== width) {
204
- frozenGlyph ??= flame(3, TINY_FLAME_PHASE);
205
- frozen = { width, lines: banner(width, t, frozenGlyph, subtitle) };
206
- }
207
- return frozen.lines;
208
- },
209
- };
210
- }
211
-
212
- export function registerHeader(pi: ExtensionAPI): void {
213
- pi.on("session_start", (_event, ctx) => {
214
- const home = homedir();
215
- const cwd = ctx.cwd.startsWith(home) ? `~${ctx.cwd.slice(home.length)}` : ctx.cwd;
216
- ctx.ui.setHeader((tui) => createBanner(tui, { version: VERSION, cwd }));
217
- });
218
- }
package/herdr-client.ts DELETED
@@ -1,60 +0,0 @@
1
- /**
2
- * herdr socket 短连接客户端:单请求单连接,只有 herdr 返回 result 才算送达。
3
- * herdr-display(身份投影)与 review(占用 state_label)共用;herdr 之外返回未送达。
4
- */
5
- import net from "node:net";
6
-
7
- const REQUEST_TIMEOUT_MS = 500;
8
-
9
- export interface HerdrPaneEnv {
10
- paneId: string;
11
- socketPath: string;
12
- }
13
-
14
- export function herdrPaneEnv(): HerdrPaneEnv | undefined {
15
- const paneId = process.env.HERDR_PANE_ID;
16
- const socketPath = process.env.HERDR_SOCKET_PATH;
17
- if (process.env.HERDR_ENV !== "1" || !paneId || !socketPath) return undefined;
18
- return { paneId, socketPath };
19
- }
20
-
21
- export function herdrRequest(
22
- source: string,
23
- method: string,
24
- params: Record<string, unknown>,
25
- ): Promise<boolean> {
26
- const env = herdrPaneEnv();
27
- if (!env) return Promise.resolve(false);
28
- const endpoint =
29
- process.platform === "win32" ? `\\\\.\\pipe\\${env.socketPath}` : env.socketPath;
30
- return new Promise((resolve) => {
31
- const socket = net.createConnection(endpoint);
32
- let buffer = "";
33
- const finish = (delivered: boolean) => {
34
- clearTimeout(timer);
35
- socket.destroy();
36
- resolve(delivered);
37
- };
38
- const timer = setTimeout(() => finish(false), REQUEST_TIMEOUT_MS);
39
- timer.unref?.();
40
- socket.on("error", () => finish(false));
41
- socket.on("end", () => finish(false));
42
- socket.on("data", (chunk) => {
43
- buffer += chunk;
44
- const end = buffer.indexOf("\n");
45
- if (end >= 0) finish(hasResult(buffer.slice(0, end)));
46
- });
47
- socket.on("connect", () => {
48
- socket.write(`${JSON.stringify({ id: source, method, params })}\n`);
49
- });
50
- });
51
- }
52
-
53
- function hasResult(line: string): boolean {
54
- try {
55
- const response = JSON.parse(line) as { result?: unknown; error?: unknown };
56
- return !response.error && typeof response.result === "object" && response.result !== null;
57
- } catch {
58
- return false;
59
- }
60
- }
package/index.ts DELETED
@@ -1,68 +0,0 @@
1
- /**
2
- * FireCode:个人 pi 定制层——启动横幅、状态栏、工具行渲染、预设、会话命名,
3
- * Claude 订阅适配、OpenAI 请求层、对抗审查与按需 Master。各功能可在 config.jsonc 的 features 里单独关闭。
4
- */
5
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
- import { type Feature, loadConfig } from "./config.js";
7
- import { registerHeader } from "./header.js";
8
- import { registerClaudeSub } from "./provider/claude-sub.js";
9
- import { registerOpenAINative } from "./provider/openai-native/index.js";
10
- import { registerPresets } from "./session/presets.js";
11
- import { registerHerdrDisplay } from "./session/herdr-display.js";
12
- import { registerSessionName } from "./session/rename.js";
13
- import { registerStats } from "./session/stats.js";
14
- import { registerStatusBar } from "./statusbar/index.js";
15
- import { registerToolRendering } from "./tools/index.js";
16
- import { registerReview } from "./review/index.js";
17
- import { registerMaster } from "./master/index.js";
18
- import { currentSubsessionRole } from "./master/role.js";
19
- import { registerWatcher } from "./watcher/index.js";
20
- import { registerRoundRecorder } from "./round-recorder.js";
21
- import { registerTruncatedWriteGuard } from "./truncated-write.js";
22
-
23
- type SimpleFeature = Exclude<Feature, "review" | "master" | "watcher" | "statusbar">;
24
-
25
- const REGISTRARS: Record<SimpleFeature, (pi: ExtensionAPI) => void> = {
26
- header: registerHeader,
27
- tools: registerToolRendering,
28
- presets: registerPresets,
29
- rename: registerSessionName,
30
- stats: registerStats,
31
- claudeSub: registerClaudeSub,
32
- openaiNative: registerOpenAINative,
33
- };
34
-
35
- /** 只属于交互主会话的功能:子会话(Worker、观察员、审查者)没有界面与命令入口,注册了只会白占资源。 */
36
- const MAIN_ONLY = new Set<SimpleFeature>(["header", "tools", "presets", "rename", "stats"]);
37
-
38
- type FirecodeSessionRole = "main" | "worker" | "observer" | "reviewer" | "advisor";
39
-
40
- export function registerFirecode(pi: ExtensionAPI, role: FirecodeSessionRole = "main"): void {
41
- const { config, problems, featuresBroken } = loadConfig();
42
- const subsession = role !== "main";
43
- const reviewEnabled = config.features.review !== false;
44
- // 轮记录不属于任何可关的功能:每个会话(含子代理)都写,界面、Master 耗时与子代理视图都只读它。
45
- registerRoundRecorder(pi);
46
- registerTruncatedWriteGuard(pi);
47
- for (const [feature, register] of Object.entries(REGISTRARS) as [SimpleFeature, (pi: ExtensionAPI) => void][]) {
48
- if (config.features[feature] === false || (subsession && MAIN_ONLY.has(feature))) continue;
49
- register(pi);
50
- }
51
- if (config.features.statusbar !== false) registerStatusBar(pi, subsession);
52
- if (config.features.watcher !== false) registerWatcher(pi, {}, subsession);
53
- if (config.features.master !== false) registerMaster(pi, {}, subsession);
54
- // herdr 显示投影没有开关:herdr 之外自我禁用,只写显示层。
55
- registerHerdrDisplay(pi, subsession);
56
- // 历史卡渲染与 checkpoint 收口不受 feature 开关控制;开关只控制命令和执行循环。
57
- // features 整节类型错误会被安全回退成全关,但那是配置坏而非用户关闭:不封存 checkpoint。
58
- registerReview(pi, reviewEnabled, featuresBroken);
59
-
60
- if (problems.length === 0) return;
61
- pi.on("session_start", (_event, ctx) => {
62
- ctx.ui.notify(`FireCode 配置有问题:${problems.join(";")}`, "warning");
63
- });
64
- }
65
-
66
- export default function firecode(pi: ExtensionAPI): void {
67
- registerFirecode(pi, currentSubsessionRole() ?? "main");
68
- }
package/jsonc.ts DELETED
@@ -1,34 +0,0 @@
1
- /** 去掉行注释和块注释,字符串内的斜杠不动。不支持尾逗号。 */
2
- export function parseJsonc(text: string): unknown {
3
- let output = "";
4
- let inString = false;
5
- let escape = false;
6
- for (let index = 0; index < text.length; index++) {
7
- const char = text[index];
8
- if (inString) {
9
- output += char;
10
- if (escape) escape = false;
11
- else if (char === "\\") escape = true;
12
- else if (char === '"') inString = false;
13
- continue;
14
- }
15
- if (char === '"') {
16
- inString = true;
17
- output += char;
18
- continue;
19
- }
20
- if (char === "/" && text[index + 1] === "/") {
21
- index += 1;
22
- while (index + 1 < text.length && text[index + 1] !== "\n") index += 1;
23
- continue;
24
- }
25
- if (char === "/" && text[index + 1] === "*") {
26
- index += 1;
27
- while (index + 1 < text.length && !(text[index] === "*" && text[index + 1] === "/")) index += 1;
28
- index += 1;
29
- continue;
30
- }
31
- output += char;
32
- }
33
- return JSON.parse(output);
34
- }