pi-firecode 0.8.1 → 1.0.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 +58 -24
- package/README.zh-CN.md +74 -0
- package/config.example.jsonc +47 -57
- package/dist/config.example.jsonc +90 -0
- package/dist/index.js +11464 -0
- package/dist/master/prompts/master.en.md +22 -0
- package/dist/master/prompts/master.zh.md +22 -0
- package/dist/master/prompts/worker.en.md +1 -0
- package/dist/master/prompts/worker.zh.md +1 -0
- package/{review → dist/review}/prompts/review.en.md +7 -6
- package/{review → dist/review}/prompts/review.zh.md +7 -6
- package/dist/watcher/prompts/watch.en.md +43 -0
- package/{watcher → dist/watcher}/prompts/watch.zh.md +1 -1
- package/package.json +10 -27
- package/config.ts +0 -474
- package/deliver.ts +0 -32
- package/flame-frames.ts +0 -460
- package/format.ts +0 -80
- package/header.ts +0 -92
- package/herdr-client.ts +0 -60
- package/index.ts +0 -62
- package/jsonc.ts +0 -34
- package/master/event-card.ts +0 -106
- package/master/event-format.ts +0 -52
- package/master/index.ts +0 -1200
- package/master/prompt.ts +0 -26
- package/master/prompts/master.zh.md +0 -17
- package/master/prompts/worker.zh.md +0 -1
- package/master/role.ts +0 -13
- package/master/spawn.ts +0 -193
- package/master/state.ts +0 -201
- package/provider/claude-sub.ts +0 -129
- package/provider/openai-native/index.ts +0 -8
- package/provider/openai-native/src/compact-client.ts +0 -362
- package/provider/openai-native/src/config.ts +0 -297
- package/provider/openai-native/src/extension.ts +0 -93
- package/provider/openai-native/src/native-compaction.ts +0 -151
- package/provider/openai-native/src/native-details.ts +0 -157
- package/provider/openai-native/src/native-replay.ts +0 -253
- package/provider/openai-native/src/native-runtime.ts +0 -144
- package/provider/openai-native/src/options.ts +0 -85
- package/provider/openai-native/src/request-pipeline.ts +0 -21
- package/provider/openai-native/src/responses-input.ts +0 -433
- package/review/advisor.ts +0 -112
- package/review/card.ts +0 -385
- package/review/checkpoint.ts +0 -384
- package/review/evidence.ts +0 -232
- package/review/index.ts +0 -1246
- package/review/outcome.ts +0 -74
- package/review/progress.ts +0 -324
- package/review/prompt.ts +0 -210
- package/review/reviewer.ts +0 -356
- package/review/session.ts +0 -129
- package/review/state.ts +0 -905
- package/review/ui.ts +0 -528
- package/session/bark.ts +0 -157
- package/session/herdr-display.ts +0 -87
- package/session/presets.ts +0 -257
- package/session/rename.ts +0 -46
- package/session/stats.ts +0 -310
- package/session/working-flame.ts +0 -116
- package/statusbar/index.ts +0 -118
- package/statusbar/quota-cache.ts +0 -60
- package/statusbar/quota-parse.ts +0 -85
- package/statusbar/quota.ts +0 -203
- package/statusbar/render.ts +0 -181
- package/statusbar/tps.ts +0 -87
- package/theme.ts +0 -84
- package/tools/grouping.ts +0 -233
- package/tools/index.ts +0 -183
- package/tools/line.ts +0 -194
- package/tools/parts.ts +0 -143
- package/tools/timing.ts +0 -22
- package/watcher/card.ts +0 -85
- package/watcher/index.ts +0 -204
- package/watcher/observer.ts +0 -75
- package/watcher/transcript.ts +0 -64
- /package/{review → dist/review}/prompts/advisor.en.md +0 -0
- /package/{review → dist/review}/prompts/advisor.zh.md +0 -0
package/statusbar/render.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/** 状态栏的纯渲染与布局:给定数据和宽度产出字符串,不触碰会话状态。 */
|
|
2
|
-
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import { clip, formatDuration, formatTokens, oneLine } from "../format.js";
|
|
5
|
-
import { cacheColor, contextColor, quotaColor } from "../theme.js";
|
|
6
|
-
import type { QuotaStatus } from "./quota-parse.js";
|
|
7
|
-
import type { TpsStatus } from "./tps.js";
|
|
8
|
-
|
|
9
|
-
export type StatusLineParts = {
|
|
10
|
-
model: string;
|
|
11
|
-
modelCompact: string;
|
|
12
|
-
quota: string;
|
|
13
|
-
quotaCompact: string;
|
|
14
|
-
context: string;
|
|
15
|
-
contextCompact: string;
|
|
16
|
-
cache: string;
|
|
17
|
-
tps: string;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
type ForegroundTheme = {
|
|
21
|
-
fg(color: ThemeColor, text: string): string;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
/** 最近一条助手消息的 cacheRead / (input + cacheRead + cacheWrite)。 */
|
|
25
|
-
export function latestCacheHitPercent(
|
|
26
|
-
entries: ReadonlyArray<{
|
|
27
|
-
type?: string;
|
|
28
|
-
message?: {
|
|
29
|
-
role?: string;
|
|
30
|
-
usage?: { input?: number; cacheRead?: number; cacheWrite?: number };
|
|
31
|
-
};
|
|
32
|
-
}>,
|
|
33
|
-
): number | undefined {
|
|
34
|
-
for (let index = entries.length - 1; index >= 0; index--) {
|
|
35
|
-
const entry = entries[index];
|
|
36
|
-
if (entry?.type !== "message" || entry.message?.role !== "assistant") continue;
|
|
37
|
-
const usage = entry.message.usage;
|
|
38
|
-
if (!usage) continue;
|
|
39
|
-
const input = usage.input ?? 0;
|
|
40
|
-
const cacheRead = usage.cacheRead ?? 0;
|
|
41
|
-
const cacheWrite = usage.cacheWrite ?? 0;
|
|
42
|
-
const promptTokens = input + cacheRead + cacheWrite;
|
|
43
|
-
if (promptTokens <= 0 || (cacheRead === 0 && cacheWrite === 0)) return undefined;
|
|
44
|
-
return (cacheRead / promptTokens) * 100;
|
|
45
|
-
}
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function renderLocation(
|
|
50
|
-
theme: ForegroundTheme,
|
|
51
|
-
directory: string,
|
|
52
|
-
branch: string | null | undefined,
|
|
53
|
-
): string {
|
|
54
|
-
return [theme.fg("dim", `📍${directory}`), branch ? theme.fg("dim", `🌿 ${branch}`) : ""]
|
|
55
|
-
.filter(Boolean)
|
|
56
|
-
.join(" · ");
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function renderQuota(
|
|
60
|
-
theme: ForegroundTheme,
|
|
61
|
-
status: QuotaStatus,
|
|
62
|
-
compact = false,
|
|
63
|
-
): string {
|
|
64
|
-
if (status.state === "loading") return theme.fg("dim", "🔋 …");
|
|
65
|
-
if (status.state === "unavailable") return theme.fg("dim", "🔋 —");
|
|
66
|
-
const windows = compact
|
|
67
|
-
? [
|
|
68
|
-
status.windows.reduce((tightest, window) =>
|
|
69
|
-
window.remaining < tightest.remaining ? window : tightest,
|
|
70
|
-
),
|
|
71
|
-
]
|
|
72
|
-
: status.windows;
|
|
73
|
-
return `${theme.fg("dim", "🔋 ")}${windows
|
|
74
|
-
.map((window) =>
|
|
75
|
-
theme.fg(quotaColor(window.remaining), `${window.label} ${window.remaining}%`),
|
|
76
|
-
)
|
|
77
|
-
.join(theme.fg("dim", "/"))}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function renderContext(
|
|
81
|
-
theme: ForegroundTheme,
|
|
82
|
-
percent: number | null | undefined,
|
|
83
|
-
contextWindow: number,
|
|
84
|
-
compact = false,
|
|
85
|
-
): string {
|
|
86
|
-
const percentText = percent == null ? "?" : `${percent.toFixed(1)}%`;
|
|
87
|
-
return `${theme.fg("dim", "📦 ")}${theme.fg(contextColor(percent), percentText)}${
|
|
88
|
-
compact ? "" : theme.fg("dim", `/${formatTokens(contextWindow)}`)
|
|
89
|
-
}`;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function renderCache(theme: ForegroundTheme, percent: number | undefined): string {
|
|
93
|
-
if (percent === undefined) return "";
|
|
94
|
-
return `${theme.fg("dim", "♻️ ")}${theme.fg(cacheColor(percent), `${Math.round(percent)}%`)}`;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function renderTps(theme: ForegroundTheme, status: TpsStatus | undefined): string {
|
|
98
|
-
if (!status) return "";
|
|
99
|
-
if (status.phase === "live") {
|
|
100
|
-
return theme.fg(
|
|
101
|
-
"success",
|
|
102
|
-
`↗ ${status.tokensPerSecond ? `${status.tokensPerSecond}t/s` : "…"}`,
|
|
103
|
-
);
|
|
104
|
-
}
|
|
105
|
-
const elapsed = formatDuration(status.elapsedSeconds * 1_000);
|
|
106
|
-
const duration = theme.fg("dim", `⏱ ${elapsed}${status.tokensPerSecond ? " · " : ""}`);
|
|
107
|
-
return status.tokensPerSecond
|
|
108
|
-
? `${duration}${theme.fg("success", `↗ ${status.tokensPerSecond}t/s`)}`
|
|
109
|
-
: duration;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** /fire-review 广播的审查进度,右对齐挂在首行末尾。 */
|
|
113
|
-
export function reviewStatus(statuses: ReadonlyMap<string, string>): string {
|
|
114
|
-
return oneLine(statuses.get("fire-review") ?? "");
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** 首行模块状态由各模块的 setStatus/onChange 驱动,状态栏只负责组合。 */
|
|
118
|
-
export function statusBadges(statuses: ReadonlyMap<string, string>, separator: string): string {
|
|
119
|
-
return [statuses.get("master"), statuses.get("watcher")].filter(Boolean).join(separator);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
export function alignRight(left: string, right: string, width: number): string {
|
|
123
|
-
if (!right) return left;
|
|
124
|
-
const padding = width - visibleWidth(left) - visibleWidth(right);
|
|
125
|
-
return padding >= 2 ? `${left}${" ".repeat(padding)}${right}` : left;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export function fitMetadataLine(
|
|
129
|
-
location: string,
|
|
130
|
-
title: string,
|
|
131
|
-
width: number,
|
|
132
|
-
separator: string,
|
|
133
|
-
badge = "",
|
|
134
|
-
): string {
|
|
135
|
-
if (width <= 0) return "";
|
|
136
|
-
// badge(如指挥官态)整段取舍:放不下就丢,不截半个,再走会话名截断阶梯。
|
|
137
|
-
if (badge) {
|
|
138
|
-
const full = [location, title, badge].filter(Boolean).join(separator);
|
|
139
|
-
if (visibleWidth(full) <= width) return full;
|
|
140
|
-
}
|
|
141
|
-
if (!title) return clip(location, width, "end", visibleWidth(location) > width ? "…" : "");
|
|
142
|
-
const full = `${location}${separator}${title}`;
|
|
143
|
-
if (visibleWidth(full) <= width) return full;
|
|
144
|
-
const titleWidth = width - visibleWidth(location) - visibleWidth(separator);
|
|
145
|
-
if (titleWidth > 0) {
|
|
146
|
-
const fittedTitle = clip(title, titleWidth, "end");
|
|
147
|
-
if (fittedTitle) return `${location}${separator}${fittedTitle}`;
|
|
148
|
-
}
|
|
149
|
-
return clip(location, width, "end");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const joinParts = (parts: string[], separator: string) => parts.filter(Boolean).join(separator);
|
|
153
|
-
|
|
154
|
-
export function fitStatusLine(
|
|
155
|
-
parts: StatusLineParts,
|
|
156
|
-
width: number,
|
|
157
|
-
separator: string,
|
|
158
|
-
): string {
|
|
159
|
-
if (width <= 0) return "";
|
|
160
|
-
// joinParts 会丢掉空的 quota/cache/tps。顺序:model → quota? → context → cache → tps。
|
|
161
|
-
const candidates = [
|
|
162
|
-
[parts.model, parts.quota, parts.context, parts.cache, parts.tps],
|
|
163
|
-
[parts.model, parts.quota, parts.context, parts.cache],
|
|
164
|
-
[parts.modelCompact, parts.quota, parts.context, parts.cache],
|
|
165
|
-
[parts.modelCompact, parts.quotaCompact, parts.context, parts.cache],
|
|
166
|
-
[parts.modelCompact, parts.context, parts.cache],
|
|
167
|
-
[parts.modelCompact, parts.context],
|
|
168
|
-
[parts.modelCompact, parts.contextCompact],
|
|
169
|
-
].map((candidate) => joinParts(candidate, separator));
|
|
170
|
-
for (const candidate of candidates) {
|
|
171
|
-
if (visibleWidth(candidate) <= width) return candidate;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const context = parts.contextCompact || parts.context;
|
|
175
|
-
const contextWidth = visibleWidth(context);
|
|
176
|
-
const modelBudget = width - contextWidth - visibleWidth(separator);
|
|
177
|
-
if (modelBudget > 0 && contextWidth <= width) {
|
|
178
|
-
return `${clip(parts.modelCompact, modelBudget, "end")}${separator}${context}`;
|
|
179
|
-
}
|
|
180
|
-
return clip(context, width, "end", "");
|
|
181
|
-
}
|
package/statusbar/tps.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/** Tracks one provider response from request start through final output usage. */
|
|
2
|
-
import { performance } from "node:perf_hooks";
|
|
3
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
|
|
5
|
-
const TOKEN_CHARS = 4;
|
|
6
|
-
const MIN_SAMPLE_MS = 1_000;
|
|
7
|
-
const UPDATE_INTERVAL_MS = 250;
|
|
8
|
-
|
|
9
|
-
export type TpsStatus =
|
|
10
|
-
| { phase: "live"; tokensPerSecond?: number }
|
|
11
|
-
| { phase: "complete"; elapsedSeconds: number; tokensPerSecond?: number };
|
|
12
|
-
|
|
13
|
-
function deltaText(event: { type: string; delta?: unknown }): string | undefined {
|
|
14
|
-
if (event.type !== "text_delta" && event.type !== "thinking_delta" && event.type !== "toolcall_delta") {
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
return typeof event.delta === "string" ? event.delta : undefined;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function registerTps(
|
|
21
|
-
pi: ExtensionAPI,
|
|
22
|
-
update: (status?: TpsStatus) => void,
|
|
23
|
-
now = (): number => performance.now(),
|
|
24
|
-
): void {
|
|
25
|
-
let requestStartedAt: number | undefined;
|
|
26
|
-
let firstDeltaAt: number | undefined;
|
|
27
|
-
let lastDeltaAt: number | undefined;
|
|
28
|
-
let lastUpdateAt: number | undefined;
|
|
29
|
-
let estimatedTokens = 0;
|
|
30
|
-
|
|
31
|
-
function resetMeasurement(): void {
|
|
32
|
-
requestStartedAt = undefined;
|
|
33
|
-
firstDeltaAt = undefined;
|
|
34
|
-
lastDeltaAt = undefined;
|
|
35
|
-
lastUpdateAt = undefined;
|
|
36
|
-
estimatedTokens = 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function clear(): void {
|
|
40
|
-
resetMeasurement();
|
|
41
|
-
update();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
pi.on("session_start", clear);
|
|
45
|
-
pi.on("model_select", clear);
|
|
46
|
-
pi.on("before_provider_request", () => {
|
|
47
|
-
resetMeasurement();
|
|
48
|
-
requestStartedAt = now();
|
|
49
|
-
});
|
|
50
|
-
pi.on("message_update", (event) => {
|
|
51
|
-
if (event.message.role !== "assistant") return;
|
|
52
|
-
const delta = deltaText(event.assistantMessageEvent);
|
|
53
|
-
if (delta === undefined) return;
|
|
54
|
-
|
|
55
|
-
const current = now();
|
|
56
|
-
lastDeltaAt = current;
|
|
57
|
-
estimatedTokens += Math.max(0, delta.length / TOKEN_CHARS);
|
|
58
|
-
if (firstDeltaAt === undefined) {
|
|
59
|
-
firstDeltaAt = current;
|
|
60
|
-
lastUpdateAt = current;
|
|
61
|
-
update({ phase: "live" });
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const elapsed = current - firstDeltaAt;
|
|
66
|
-
if (elapsed < MIN_SAMPLE_MS || current - (lastUpdateAt ?? 0) < UPDATE_INTERVAL_MS) return;
|
|
67
|
-
lastUpdateAt = current;
|
|
68
|
-
const officialTokens = event.message.usage.output;
|
|
69
|
-
const tokens = officialTokens > 0 ? officialTokens : Math.round(estimatedTokens);
|
|
70
|
-
update({ phase: "live", tokensPerSecond: Math.round((tokens * 1_000) / elapsed) });
|
|
71
|
-
});
|
|
72
|
-
pi.on("message_end", (event) => {
|
|
73
|
-
if (event.message.role !== "assistant" || firstDeltaAt === undefined || lastDeltaAt === undefined) return;
|
|
74
|
-
const current = now();
|
|
75
|
-
const generationMs = lastDeltaAt - firstDeltaAt;
|
|
76
|
-
const outputTokens = event.message.usage.output;
|
|
77
|
-
update({
|
|
78
|
-
phase: "complete",
|
|
79
|
-
elapsedSeconds: Math.max(0, (current - (requestStartedAt ?? firstDeltaAt)) / 1_000),
|
|
80
|
-
...(generationMs > 0 && outputTokens > 0
|
|
81
|
-
? { tokensPerSecond: Math.round((outputTokens * 1_000) / generationMs) }
|
|
82
|
-
: {}),
|
|
83
|
-
});
|
|
84
|
-
resetMeasurement();
|
|
85
|
-
});
|
|
86
|
-
pi.on("session_shutdown", clear);
|
|
87
|
-
}
|
package/theme.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/** FireCode 品牌色与数值分级配色:所有阈值到颜色的映射集中在此。 */
|
|
2
|
-
import type { ExtensionAPI, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
|
|
4
|
-
export const ANSI = {
|
|
5
|
-
reset: "\x1b[0m",
|
|
6
|
-
bold: "\x1b[1m",
|
|
7
|
-
} as const;
|
|
8
|
-
|
|
9
|
-
const rgb = (red: number, green: number, blue: number) =>
|
|
10
|
-
`\x1b[38;2;${red};${green};${blue}m`;
|
|
11
|
-
|
|
12
|
-
/** 火焰渐变:横幅按行取色,从底部暗红烧到芯部白金。 */
|
|
13
|
-
export const FLAME = {
|
|
14
|
-
red: rgb(255, 47, 32),
|
|
15
|
-
orange: rgb(255, 119, 28),
|
|
16
|
-
gold: rgb(255, 195, 61),
|
|
17
|
-
white: rgb(255, 239, 184),
|
|
18
|
-
textTop: rgb(255, 112, 38),
|
|
19
|
-
textMid: rgb(255, 184, 62),
|
|
20
|
-
textCore: rgb(255, 236, 170),
|
|
21
|
-
} as const;
|
|
22
|
-
|
|
23
|
-
/** 阈值表:首个满足 `value >= at` 的档位胜出,表按 at 降序书写。 */
|
|
24
|
-
type Threshold = { at: number; color: ThemeColor };
|
|
25
|
-
|
|
26
|
-
function pick(value: number, thresholds: readonly Threshold[]): ThemeColor {
|
|
27
|
-
for (const threshold of thresholds) {
|
|
28
|
-
if (value >= threshold.at) return threshold.color;
|
|
29
|
-
}
|
|
30
|
-
return thresholds[thresholds.length - 1].color;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const CONTEXT_FILL = [
|
|
34
|
-
{ at: 75, color: "error" },
|
|
35
|
-
{ at: 50, color: "warning" },
|
|
36
|
-
{ at: 0, color: "success" },
|
|
37
|
-
] as const satisfies readonly Threshold[];
|
|
38
|
-
|
|
39
|
-
/** 缓存命中越高越好,与上下文填充方向相反。 */
|
|
40
|
-
const CACHE_HIT = [
|
|
41
|
-
{ at: 90, color: "success" },
|
|
42
|
-
{ at: 50, color: "muted" },
|
|
43
|
-
{ at: 20, color: "warning" },
|
|
44
|
-
{ at: 0, color: "error" },
|
|
45
|
-
] as const satisfies readonly Threshold[];
|
|
46
|
-
|
|
47
|
-
const QUOTA_REMAINING = [
|
|
48
|
-
{ at: 51, color: "success" },
|
|
49
|
-
{ at: 26, color: "warning" },
|
|
50
|
-
{ at: 0, color: "error" },
|
|
51
|
-
] as const satisfies readonly Threshold[];
|
|
52
|
-
|
|
53
|
-
const RESULT_SIZE = [
|
|
54
|
-
{ at: 50_000, color: "error" },
|
|
55
|
-
{ at: 10_000, color: "warning" },
|
|
56
|
-
{ at: 0, color: "dim" },
|
|
57
|
-
] as const satisfies readonly Threshold[];
|
|
58
|
-
|
|
59
|
-
export const contextColor = (percent: number | null | undefined): ThemeColor =>
|
|
60
|
-
percent == null ? "muted" : pick(percent, CONTEXT_FILL);
|
|
61
|
-
|
|
62
|
-
export const cacheColor = (percent: number): ThemeColor =>
|
|
63
|
-
pick(percent, CACHE_HIT);
|
|
64
|
-
|
|
65
|
-
export const quotaColor = (remaining: number): ThemeColor =>
|
|
66
|
-
pick(remaining, QUOTA_REMAINING);
|
|
67
|
-
|
|
68
|
-
export const sizeColor = (chars: number): ThemeColor =>
|
|
69
|
-
pick(chars, RESULT_SIZE);
|
|
70
|
-
|
|
71
|
-
type ThinkingLevel = ReturnType<ExtensionAPI["getThinkingLevel"]>;
|
|
72
|
-
|
|
73
|
-
const THINKING: Record<ThinkingLevel, ThemeColor> = {
|
|
74
|
-
off: "thinkingOff",
|
|
75
|
-
minimal: "thinkingMinimal",
|
|
76
|
-
low: "thinkingLow",
|
|
77
|
-
medium: "thinkingMedium",
|
|
78
|
-
high: "thinkingHigh",
|
|
79
|
-
xhigh: "thinkingXhigh",
|
|
80
|
-
max: "thinkingMax",
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
export const thinkingColor = (level: ThinkingLevel): ThemeColor =>
|
|
84
|
-
THINKING[level];
|
package/tools/grouping.ts
DELETED
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 连续工具行的视觉合并与第三方工具通用单行兜底:
|
|
3
|
-
* 1. 原生内置或声明了 self 的工具:保留自渲染,去除相邻空行形成轨道;
|
|
4
|
-
* 2. 未知第三方或默认宽框工具:自动收口为通用 ToolLine 单行紧凑轨道与全宽背景。
|
|
5
|
-
*/
|
|
6
|
-
import type { ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { ToolLine } from "./line.js";
|
|
8
|
-
import { genericArgsParts } from "./parts.js";
|
|
9
|
-
|
|
10
|
-
const GROUP_PATCH = Symbol.for("pi.firecode.group-patch");
|
|
11
|
-
const REQUEST_RENDER_PATCH = Symbol.for("pi.firecode.request-render-patch");
|
|
12
|
-
const GLOBAL_STATE = Symbol.for("pi.firecode.tools-state");
|
|
13
|
-
const THEME_KEY = Symbol.for("@earendil-works/pi-coding-agent:theme");
|
|
14
|
-
|
|
15
|
-
function getTheme(): Theme {
|
|
16
|
-
const globalTheme = (globalThis as any)[THEME_KEY];
|
|
17
|
-
if (globalTheme && typeof globalTheme.fg === "function") return globalTheme;
|
|
18
|
-
return {
|
|
19
|
-
fg: (_color: string, text: string) => text,
|
|
20
|
-
bg: (_color: string, text: string) => text,
|
|
21
|
-
bold: (text: string) => text,
|
|
22
|
-
} as unknown as Theme;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
type RenderFunction = (width: number) => string[];
|
|
26
|
-
type RequestRenderFunction = (force?: boolean) => unknown;
|
|
27
|
-
type ToolRow = { toolName: unknown; setExpanded: unknown; render: RenderFunction };
|
|
28
|
-
type ToolRowPatch = { original: RenderFunction; patched: RenderFunction };
|
|
29
|
-
type ToolRowPrototype = ToolRow & { [GROUP_PATCH]?: ToolRowPatch };
|
|
30
|
-
type RequestRenderPatch = { original: RequestRenderFunction; patched: RequestRenderFunction };
|
|
31
|
-
type ContainerLike = { children: unknown[] };
|
|
32
|
-
type TuiLike = {
|
|
33
|
-
requestRender: RequestRenderFunction;
|
|
34
|
-
children?: unknown[];
|
|
35
|
-
[REQUEST_RENDER_PATCH]?: RequestRenderPatch;
|
|
36
|
-
};
|
|
37
|
-
type GlobalState = {
|
|
38
|
-
tui?: TuiLike;
|
|
39
|
-
chat?: ContainerLike;
|
|
40
|
-
indexedChildren?: unknown[];
|
|
41
|
-
indexedLength?: number;
|
|
42
|
-
joinedRows?: WeakSet<object>;
|
|
43
|
-
patchedPrototype?: ToolRowPrototype;
|
|
44
|
-
patchErrorReported?: boolean;
|
|
45
|
-
decorated: Set<string>;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
// 扩展热重载会重新执行模块,patch 状态必须挂在全局才能正确卸载。
|
|
49
|
-
const runtimeGlobal = globalThis as typeof globalThis & { [GLOBAL_STATE]?: GlobalState };
|
|
50
|
-
const globalState = (runtimeGlobal[GLOBAL_STATE] ??= { decorated: new Set<string>() });
|
|
51
|
-
|
|
52
|
-
function isToolRow(value: unknown): value is ToolRow {
|
|
53
|
-
if (!value || typeof value !== "object") return false;
|
|
54
|
-
const row = value as ToolRow;
|
|
55
|
-
return typeof row.render === "function" && typeof row.setExpanded === "function" && "toolName" in row;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function isDecoratedToolRow(value: unknown): value is ToolRow {
|
|
59
|
-
return isToolRow(value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function renderFallbackToolRow(row: any, width: number, customTheme?: Theme): string[] {
|
|
63
|
-
const isError = row.result?.isError ?? false;
|
|
64
|
-
const isPartial = row.isPartial ?? false;
|
|
65
|
-
const toolName = typeof row.toolName === "string" ? row.toolName : "tool";
|
|
66
|
-
const activeTheme = customTheme ?? getTheme();
|
|
67
|
-
const toolLine = new ToolLine({
|
|
68
|
-
label: toolName,
|
|
69
|
-
value: genericArgsParts(row.args),
|
|
70
|
-
clip: "end",
|
|
71
|
-
theme: activeTheme,
|
|
72
|
-
ctx: {
|
|
73
|
-
state: row.rendererState ?? {},
|
|
74
|
-
cwd: row.cwd ?? process.cwd(),
|
|
75
|
-
toolCallId: row.toolCallId ?? "",
|
|
76
|
-
isPartial,
|
|
77
|
-
isError,
|
|
78
|
-
expanded: row.expanded ?? false,
|
|
79
|
-
},
|
|
80
|
-
});
|
|
81
|
-
const lines = ["", ...toolLine.render(width)];
|
|
82
|
-
if (row.expanded && row.result) {
|
|
83
|
-
const textContent = Array.isArray(row.result.content)
|
|
84
|
-
? row.result.content
|
|
85
|
-
.map((c: any) => (c?.type === "text" && typeof c.text === "string" ? c.text : ""))
|
|
86
|
-
.filter(Boolean)
|
|
87
|
-
.join("\n")
|
|
88
|
-
: "";
|
|
89
|
-
if (textContent) {
|
|
90
|
-
const outputLines = textContent.split("\n").map((l: string) => activeTheme.fg("toolOutput", l));
|
|
91
|
-
lines.push("", ...outputLines);
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return lines;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function findChatContainer(value: unknown, seen = new Set<unknown>()): ContainerLike | undefined {
|
|
98
|
-
if (!value || typeof value !== "object" || seen.has(value)) return undefined;
|
|
99
|
-
seen.add(value);
|
|
100
|
-
const children = (value as ContainerLike).children;
|
|
101
|
-
if (!Array.isArray(children)) return undefined;
|
|
102
|
-
if (children.some(isToolRow)) return value as ContainerLike;
|
|
103
|
-
for (const child of children) {
|
|
104
|
-
const found = findChatContainer(child, seen);
|
|
105
|
-
if (found) return found;
|
|
106
|
-
}
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** 增量标记"紧跟另一条装饰行"的行,聊天记录只增不改,可以从上次位置续算。 */
|
|
111
|
-
function indexJoinedRows(children: unknown[]): void {
|
|
112
|
-
const canAppend =
|
|
113
|
-
globalState.indexedChildren === children &&
|
|
114
|
-
globalState.indexedLength !== undefined &&
|
|
115
|
-
globalState.indexedLength <= children.length &&
|
|
116
|
-
globalState.joinedRows !== undefined;
|
|
117
|
-
const joinedRows =
|
|
118
|
-
canAppend && globalState.joinedRows ? globalState.joinedRows : new WeakSet<object>();
|
|
119
|
-
const start = canAppend ? Math.max(1, globalState.indexedLength ?? 1) : 1;
|
|
120
|
-
for (let index = start; index < children.length; index++) {
|
|
121
|
-
const current = children[index];
|
|
122
|
-
if (isDecoratedToolRow(current) && isDecoratedToolRow(children[index - 1]))
|
|
123
|
-
joinedRows.add(current);
|
|
124
|
-
}
|
|
125
|
-
globalState.indexedChildren = children;
|
|
126
|
-
globalState.indexedLength = children.length;
|
|
127
|
-
globalState.joinedRows = joinedRows;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function followsToolRow(row: ToolRow): boolean {
|
|
131
|
-
if (!globalState.chat) globalState.chat = findChatContainer(globalState.tui);
|
|
132
|
-
const children = globalState.chat?.children;
|
|
133
|
-
if (!children) return false;
|
|
134
|
-
if (globalState.indexedChildren !== children || globalState.indexedLength !== children.length)
|
|
135
|
-
indexJoinedRows(children);
|
|
136
|
-
return globalState.joinedRows?.has(row) ?? false;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/** 走 pi 原生渲染(会消费工具自带 renderCall)的工具;其余走通用兜底行。
|
|
140
|
-
* subagents 自带中文 ToolLine 渲染(master/index.ts),不列入会被兜底行遮掉。 */
|
|
141
|
-
export const FIRECODE_TOOLS = new Set(["read", "bash", "edit", "write", "find", "grep", "ls", "subagents"]);
|
|
142
|
-
|
|
143
|
-
function patchToolRows(): void {
|
|
144
|
-
if (globalState.patchedPrototype) return;
|
|
145
|
-
const chat = globalState.chat ?? findChatContainer(globalState.tui);
|
|
146
|
-
if (!chat) return;
|
|
147
|
-
globalState.chat = chat;
|
|
148
|
-
const row = chat.children.find(isToolRow);
|
|
149
|
-
if (!row) return;
|
|
150
|
-
const prototype = Object.getPrototypeOf(row) as ToolRowPrototype;
|
|
151
|
-
if (prototype[GROUP_PATCH]) {
|
|
152
|
-
globalState.patchedPrototype = prototype;
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const original = prototype.render;
|
|
156
|
-
const patched: RenderFunction = function (this: ToolRow, width: number): string[] {
|
|
157
|
-
const toolName = typeof this.toolName === "string" ? this.toolName : "";
|
|
158
|
-
const isFirecodeTool = FIRECODE_TOOLS.has(toolName);
|
|
159
|
-
const isExpanded = Boolean((this as any).expanded);
|
|
160
|
-
const lines = isFirecodeTool || isExpanded
|
|
161
|
-
? original.call(this, width)
|
|
162
|
-
: renderFallbackToolRow(this, width);
|
|
163
|
-
return followsToolRow(this) && lines[0] === "" ? lines.slice(1) : lines;
|
|
164
|
-
};
|
|
165
|
-
Object.defineProperty(prototype, GROUP_PATCH, {
|
|
166
|
-
configurable: true,
|
|
167
|
-
value: { original, patched } satisfies ToolRowPatch,
|
|
168
|
-
});
|
|
169
|
-
prototype.render = patched;
|
|
170
|
-
globalState.patchedPrototype = prototype;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function installGroupPatch(ui: ExtensionUIContext, decorated: Set<string>): void {
|
|
174
|
-
globalState.decorated = decorated;
|
|
175
|
-
globalState.patchErrorReported = false;
|
|
176
|
-
// 借一次 widget 生命周期拿到 tui 实例,拿到即注销。
|
|
177
|
-
ui.setWidget("firecode-tui-capture", (tui) => {
|
|
178
|
-
const patchableTui = tui as TuiLike;
|
|
179
|
-
globalState.tui = patchableTui;
|
|
180
|
-
globalState.chat = undefined;
|
|
181
|
-
globalState.indexedChildren = undefined;
|
|
182
|
-
globalState.joinedRows = undefined;
|
|
183
|
-
if (!patchableTui[REQUEST_RENDER_PATCH]) {
|
|
184
|
-
const original = patchableTui.requestRender;
|
|
185
|
-
const patched: RequestRenderFunction = (force) => {
|
|
186
|
-
try {
|
|
187
|
-
patchToolRows();
|
|
188
|
-
} catch (error) {
|
|
189
|
-
if (!globalState.patchErrorReported) {
|
|
190
|
-
globalState.patchErrorReported = true;
|
|
191
|
-
ui.notify(
|
|
192
|
-
`工具轨道连接失败:${error instanceof Error ? error.message : String(error)}`,
|
|
193
|
-
"warning",
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return original.call(patchableTui, force);
|
|
198
|
-
};
|
|
199
|
-
Object.defineProperty(patchableTui, REQUEST_RENDER_PATCH, {
|
|
200
|
-
configurable: true,
|
|
201
|
-
value: { original, patched } satisfies RequestRenderPatch,
|
|
202
|
-
});
|
|
203
|
-
patchableTui.requestRender = patched;
|
|
204
|
-
}
|
|
205
|
-
patchToolRows();
|
|
206
|
-
return { render: () => [], invalidate: () => {} };
|
|
207
|
-
});
|
|
208
|
-
ui.setWidget("firecode-tui-capture", undefined);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function uninstallGroupPatch(): void {
|
|
212
|
-
const tui = globalState.tui;
|
|
213
|
-
const requestPatch = tui?.[REQUEST_RENDER_PATCH];
|
|
214
|
-
if (tui && requestPatch && tui.requestRender === requestPatch.patched) {
|
|
215
|
-
tui.requestRender = requestPatch.original;
|
|
216
|
-
delete tui[REQUEST_RENDER_PATCH];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const prototype = globalState.patchedPrototype;
|
|
220
|
-
const rowPatch = prototype?.[GROUP_PATCH];
|
|
221
|
-
if (prototype && rowPatch && prototype.render === rowPatch.patched) {
|
|
222
|
-
prototype.render = rowPatch.original;
|
|
223
|
-
delete prototype[GROUP_PATCH];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
globalState.tui = undefined;
|
|
227
|
-
globalState.chat = undefined;
|
|
228
|
-
globalState.indexedChildren = undefined;
|
|
229
|
-
globalState.indexedLength = undefined;
|
|
230
|
-
globalState.joinedRows = undefined;
|
|
231
|
-
globalState.patchedPrototype = undefined;
|
|
232
|
-
globalState.patchErrorReported = false;
|
|
233
|
-
}
|