@buyi1net/pi-toolkit 0.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.
Files changed (129) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/assembler.ts +127 -0
  4. package/config.ts +201 -0
  5. package/i18n.ts +693 -0
  6. package/index.ts +99 -0
  7. package/menu/items.ts +264 -0
  8. package/menu/panels.ts +91 -0
  9. package/menu/settings-list.ts +84 -0
  10. package/menu/theme.ts +31 -0
  11. package/menu/toolkit-menu.ts +139 -0
  12. package/module.ts +156 -0
  13. package/modules/eyes/chain.ts +173 -0
  14. package/modules/eyes/config.ts +319 -0
  15. package/modules/eyes/index.ts +161 -0
  16. package/modules/eyes/menu.ts +828 -0
  17. package/modules/eyes/pi-model-backend.ts +245 -0
  18. package/modules/eyes/resilience.ts +161 -0
  19. package/modules/eyes/vision-bridge.ts +232 -0
  20. package/modules/eyes/vision-cache.ts +86 -0
  21. package/modules/eyes/vision-json.ts +19 -0
  22. package/modules/eyes/vision-preprocess.ts +192 -0
  23. package/modules/eyes/vision-probe.ts +21 -0
  24. package/modules/eyes/vision-prompt.ts +90 -0
  25. package/modules/eyes/vision-tool.ts +59 -0
  26. package/modules/eyes/vision-types.ts +33 -0
  27. package/modules/index.ts +10 -0
  28. package/modules/subagents/agents/researcher.md +51 -0
  29. package/modules/subagents/agents/scout.md +40 -0
  30. package/modules/subagents/agents/worker.md +79 -0
  31. package/modules/subagents/config.json.example +6 -0
  32. package/modules/subagents/config.ts +144 -0
  33. package/modules/subagents/index.ts +91 -0
  34. package/modules/subagents/menu.ts +388 -0
  35. package/modules/subagents/src/activity.ts +511 -0
  36. package/modules/subagents/src/agents.ts +126 -0
  37. package/modules/subagents/src/command.ts +37 -0
  38. package/modules/subagents/src/dependencies.ts +246 -0
  39. package/modules/subagents/src/diagnostics.ts +13 -0
  40. package/modules/subagents/src/display.ts +94 -0
  41. package/modules/subagents/src/headless.ts +342 -0
  42. package/modules/subagents/src/herdr.ts +203 -0
  43. package/modules/subagents/src/index.ts +2798 -0
  44. package/modules/subagents/src/inspect-tool.ts +839 -0
  45. package/modules/subagents/src/launch-config.ts +196 -0
  46. package/modules/subagents/src/layout-budget.ts +26 -0
  47. package/modules/subagents/src/list-tool.ts +292 -0
  48. package/modules/subagents/src/message-tool.ts +808 -0
  49. package/modules/subagents/src/names.ts +17 -0
  50. package/modules/subagents/src/pane-layout.ts +49 -0
  51. package/modules/subagents/src/params.ts +145 -0
  52. package/modules/subagents/src/renderers.ts +181 -0
  53. package/modules/subagents/src/result.ts +43 -0
  54. package/modules/subagents/src/retention.ts +114 -0
  55. package/modules/subagents/src/route-error.ts +212 -0
  56. package/modules/subagents/src/routing.ts +235 -0
  57. package/modules/subagents/src/runtime-registry.ts +159 -0
  58. package/modules/subagents/src/session.ts +801 -0
  59. package/modules/subagents/src/status.ts +513 -0
  60. package/modules/subagents/src/stop-tool.ts +235 -0
  61. package/modules/subagents/src/subagent-done.ts +590 -0
  62. package/modules/subagents/src/subagent-tool.ts +1155 -0
  63. package/modules/subagents/src/surface.ts +355 -0
  64. package/modules/subagents/src/team-dispatch-tool.ts +219 -0
  65. package/modules/subagents/src/team.ts +232 -0
  66. package/modules/subagents/src/tmux.ts +210 -0
  67. package/modules/subagents/src/tools/safe-bash.ts +72 -0
  68. package/modules/subagents/src/types.ts +131 -0
  69. package/modules/tui/adapter/provider-usage.ts +143 -0
  70. package/modules/tui/config.ts +50 -0
  71. package/modules/tui/index.ts +120 -0
  72. package/modules/tui/kernel/pkg/shared/grok-subscription.ts +169 -0
  73. package/modules/tui/kernel/pkg/shared/official-subscription.ts +237 -0
  74. package/modules/tui/kernel/pkg/shared/provider-catalog.ts +367 -0
  75. package/modules/tui/kernel/pkg/shared/provider-contracts.ts +150 -0
  76. package/modules/tui/kernel/pkg/shared/provider-display.ts +55 -0
  77. package/modules/tui/kernel/pkg/shared/provider-parsers.ts +171 -0
  78. package/modules/tui/kernel/pkg/shared/volcengine.ts +191 -0
  79. package/modules/tui/kernel/pkg/shared/zhipu.ts +149 -0
  80. package/modules/tui/kernel/pkg/usage-core/index.ts +335 -0
  81. package/modules/tui/kernel/pkg/usage-core/provider-routes.ts +187 -0
  82. package/modules/tui/kernel/pkg/usage-node/index.ts +635 -0
  83. package/modules/tui/kernel/pkg/usage-node/provider-usage.ts +388 -0
  84. package/modules/tui/kernel/usage-core.ts +2 -0
  85. package/modules/tui/kernel/usage-node.ts +2 -0
  86. package/modules/tui/menu.ts +418 -0
  87. package/modules/tui/plugin/editor.ts +396 -0
  88. package/modules/tui/plugin/footer.ts +161 -0
  89. package/modules/tui/plugin/index.ts +30 -0
  90. package/modules/tui/plugin/lifecycle.ts +637 -0
  91. package/modules/tui/plugin/package-order.ts +169 -0
  92. package/modules/tui/plugin/screen-transition.ts +203 -0
  93. package/modules/tui/plugin/settings-config.ts +297 -0
  94. package/modules/tui/plugin/status-sources.ts +49 -0
  95. package/modules/tui/plugin/transition-gate.ts +261 -0
  96. package/modules/tui/renderer/custom-header.ts +157 -0
  97. package/modules/tui/renderer/editor.ts +70 -0
  98. package/modules/tui/renderer/header.ts +72 -0
  99. package/modules/tui/renderer/icons.ts +149 -0
  100. package/modules/tui/renderer/pi-installer-logo.ts +194 -0
  101. package/modules/tui/status/auto-compaction.ts +67 -0
  102. package/modules/tui/status/project-status.ts +655 -0
  103. package/modules/tui/status/provider-status.ts +120 -0
  104. package/modules/tui/status/runtime-status.ts +307 -0
  105. package/modules/tui/status/session-status.ts +325 -0
  106. package/modules/tui/status/status-config.ts +95 -0
  107. package/modules/tui/status/status-segments.ts +263 -0
  108. package/modules/tui/status/turn-telemetry.ts +466 -0
  109. package/modules/tui/themes/LICENSE.pi-themes-bundle +21 -0
  110. package/modules/tui/themes/UPSTREAM.md +7 -0
  111. package/modules/tui/themes/catppuccin-latte.json +80 -0
  112. package/modules/tui/themes/catppuccin-mocha.json +79 -0
  113. package/modules/tui/themes/crimson-noir.json +85 -0
  114. package/modules/tui/themes/dracula.json +79 -0
  115. package/modules/tui/themes/everforest-dark.json +85 -0
  116. package/modules/tui/themes/gruvbox-dark.json +85 -0
  117. package/modules/tui/themes/gruvbox-light.json +85 -0
  118. package/modules/tui/themes/matrix.json +85 -0
  119. package/modules/tui/themes/nord.json +85 -0
  120. package/modules/tui/themes/one-dark.json +85 -0
  121. package/modules/tui/themes/rose-pine-dawn.json +85 -0
  122. package/modules/tui/themes/rose-pine.json +85 -0
  123. package/modules/tui/themes/solarized-dark.json +85 -0
  124. package/modules/tui/themes/solarized-light.json +85 -0
  125. package/modules/tui/themes/tokyo-night-storm.json +79 -0
  126. package/modules/tui/themes/tokyo-night.json +79 -0
  127. package/package.json +28 -0
  128. package/services.ts +38 -0
  129. package/toolkit.ts +147 -0
@@ -0,0 +1,466 @@
1
+ import type {
2
+ AgentEndEvent,
3
+ AgentSettledEvent,
4
+ AgentStartEvent,
5
+ ExtensionAPI,
6
+ ExtensionContext,
7
+ SessionEntry,
8
+ MessageEndEvent,
9
+ MessageStartEvent,
10
+ MessageUpdateEvent,
11
+ Theme,
12
+ TurnEndEvent,
13
+ TurnStartEvent,
14
+ } from "@earendil-works/pi-coding-agent";
15
+ import { Text } from "@earendil-works/pi-tui";
16
+ import { resolveGlyphs, type IconGlyphs } from "../renderer/icons.ts";
17
+ import { formatTokenCount } from "./session-status.ts";
18
+ import { formatElapsed, type TurnTimerController } from "./status-segments.ts";
19
+
20
+ export const TURN_TELEMETRY_ENTRY_TYPE = "pi-tui.turn-telemetry";
21
+ export const TURN_DURATION_ENTRY_TYPE = "pi-tui.turn-duration";
22
+
23
+ type TurnTelemetryEvent =
24
+ | AgentStartEvent
25
+ | AgentSettledEvent
26
+ | TurnStartEvent
27
+ | MessageStartEvent
28
+ | MessageUpdateEvent
29
+ | MessageEndEvent
30
+ | TurnEndEvent;
31
+
32
+ type AgentMessage = MessageStartEvent["message"];
33
+ type AssistantMessage = Extract<AgentMessage, { role: "assistant" }>;
34
+
35
+ interface MessageTiming {
36
+ firstOutputMs: number | null;
37
+ }
38
+
39
+ interface TurnTiming {
40
+ startMs: number;
41
+ firstTokenMs: number | null;
42
+ currentMessage: MessageTiming | null;
43
+ messages: AssistantMessage[];
44
+ generationMs: number;
45
+ }
46
+
47
+ export interface TurnTelemetrySnapshot {
48
+ tokensPerSecond: number | null;
49
+ ttftMs: number;
50
+ totalMs: number;
51
+ inputTokens: number;
52
+ outputTokens: number;
53
+ cacheReadTokens: number | null;
54
+ cacheWriteTokens: number | null;
55
+ generationMs: number;
56
+ costUsd: number;
57
+ }
58
+
59
+ export interface PersistedTurnTelemetry {
60
+ schemaVersion: 1;
61
+ telemetry: TurnTelemetrySnapshot;
62
+ }
63
+
64
+ export interface PersistedTurnDuration {
65
+ schemaVersion: 1;
66
+ elapsedMs: number;
67
+ }
68
+
69
+ function isRecord(value: unknown): value is Record<string, unknown> {
70
+ return typeof value === "object" && value !== null;
71
+ }
72
+
73
+ function isNonNegativeFinite(value: unknown): value is number {
74
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
75
+ }
76
+
77
+ function isNullableNonNegativeFinite(value: unknown): value is number | null {
78
+ return value === null || isNonNegativeFinite(value);
79
+ }
80
+
81
+ function isTurnTelemetrySnapshot(value: unknown): value is TurnTelemetrySnapshot {
82
+ if (!isRecord(value)) return false;
83
+ return (
84
+ isNullableNonNegativeFinite(value.tokensPerSecond) &&
85
+ isNonNegativeFinite(value.ttftMs) &&
86
+ isNonNegativeFinite(value.totalMs) &&
87
+ isNonNegativeFinite(value.inputTokens) &&
88
+ isNonNegativeFinite(value.outputTokens) &&
89
+ (value.cacheReadTokens === undefined || isNullableNonNegativeFinite(value.cacheReadTokens)) &&
90
+ (value.cacheWriteTokens === undefined || isNullableNonNegativeFinite(value.cacheWriteTokens)) &&
91
+ isNonNegativeFinite(value.generationMs) &&
92
+ isNonNegativeFinite(value.costUsd)
93
+ );
94
+ }
95
+
96
+ export function createTurnTelemetryEntryData(
97
+ telemetry: TurnTelemetrySnapshot,
98
+ ): PersistedTurnTelemetry {
99
+ return { schemaVersion: 1, telemetry: { ...telemetry } };
100
+ }
101
+
102
+ export function readTurnTelemetryEntryData(value: unknown): TurnTelemetrySnapshot | undefined {
103
+ if (!isRecord(value) || value.schemaVersion !== 1 || !isTurnTelemetrySnapshot(value.telemetry)) {
104
+ return undefined;
105
+ }
106
+ return {
107
+ ...value.telemetry,
108
+ cacheReadTokens: value.telemetry.cacheReadTokens ?? null,
109
+ cacheWriteTokens: value.telemetry.cacheWriteTokens ?? null,
110
+ };
111
+ }
112
+
113
+ export function createTurnDurationEntryData(elapsedMs: number): PersistedTurnDuration {
114
+ return { schemaVersion: 1, elapsedMs: Math.max(0, elapsedMs) };
115
+ }
116
+
117
+ export function readTurnDurationEntryData(value: unknown): number | undefined {
118
+ if (!isRecord(value) || value.schemaVersion !== 1 || !isNonNegativeFinite(value.elapsedMs)) {
119
+ return undefined;
120
+ }
121
+ return value.elapsedMs;
122
+ }
123
+
124
+ export function readLatestTurnDuration(entries: readonly SessionEntry[]): number | undefined {
125
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
126
+ const entry = entries[index];
127
+ if (entry?.type !== "custom") continue;
128
+ if (entry.customType === TURN_DURATION_ENTRY_TYPE) {
129
+ const elapsedMs = readTurnDurationEntryData(entry.data);
130
+ if (elapsedMs !== undefined) return elapsedMs;
131
+ }
132
+ if (entry.customType === TURN_TELEMETRY_ENTRY_TYPE) {
133
+ const telemetry = readTurnTelemetryEntryData(entry.data);
134
+ if (telemetry) return telemetry.totalMs;
135
+ }
136
+ }
137
+ return undefined;
138
+ }
139
+
140
+ export function registerTurnTelemetryRenderers(
141
+ pi: Pick<ExtensionAPI, "registerEntryRenderer">,
142
+ getGlyphs: () => IconGlyphs,
143
+ ): void {
144
+ pi.registerEntryRenderer?.<PersistedTurnTelemetry>(
145
+ TURN_TELEMETRY_ENTRY_TYPE,
146
+ (entry, _options, theme) => {
147
+ const telemetry = readTurnTelemetryEntryData(entry.data);
148
+ if (!telemetry) return undefined;
149
+ return new Text(formatTurnTelemetry(telemetry, theme, getGlyphs()), 1, 0);
150
+ },
151
+ );
152
+ pi.registerEntryRenderer?.<PersistedTurnDuration>(TURN_DURATION_ENTRY_TYPE, () => undefined);
153
+ }
154
+
155
+ interface TurnTelemetryDependencies {
156
+ isEnabled: () => boolean;
157
+ getTimer: () => Pick<TurnTimerController, "start" | "end" | "restore"> | undefined;
158
+ appendEntry: ExtensionAPI["appendEntry"];
159
+ }
160
+
161
+ export class TurnTelemetryController {
162
+ private readonly dependencies: TurnTelemetryDependencies;
163
+ private readonly tracker: TurnTelemetryTracker;
164
+
165
+ constructor(dependencies: TurnTelemetryDependencies, now?: () => number) {
166
+ this.dependencies = dependencies;
167
+ this.tracker = new TurnTelemetryTracker(now);
168
+ }
169
+
170
+ handle(event: Exclude<TurnTelemetryEvent, AgentSettledEvent> | AgentEndEvent): void {
171
+ const { isEnabled, getTimer, appendEntry } = this.dependencies;
172
+ // 耗时段独立于回复尾遥测开关,禁用采集仍需冻结并恢复本轮耗时。
173
+ if (event.type === "agent_end") {
174
+ const elapsedMs = getTimer()?.end();
175
+ if (elapsedMs !== undefined) {
176
+ appendEntry(TURN_DURATION_ENTRY_TYPE, createTurnDurationEntryData(elapsedMs));
177
+ }
178
+ return;
179
+ }
180
+ if (isEnabled()) this.tracker.handle(event);
181
+ if (event.type === "agent_start") getTimer()?.start();
182
+ }
183
+
184
+ settle(event: AgentSettledEvent, mode: ExtensionContext["mode"]): void {
185
+ const { isEnabled, getTimer, appendEntry } = this.dependencies;
186
+ if (!isEnabled()) return;
187
+ // agent_end 后宿主可能继续重试;settled 才是整次回复的落盘时机。
188
+ const telemetry = this.tracker.handle(event);
189
+ if (!telemetry || mode !== "tui") return;
190
+ getTimer()?.restore(telemetry.totalMs);
191
+ appendEntry(TURN_TELEMETRY_ENTRY_TYPE, createTurnTelemetryEntryData(telemetry));
192
+ }
193
+
194
+ reset(): void {
195
+ // 计时器由装配层销毁;这里只清除尚未持久化的回复,避免跨会话串账。
196
+ this.tracker.reset();
197
+ }
198
+ }
199
+
200
+ function isAssistantMessage(message: AgentMessage): message is AssistantMessage {
201
+ return message.role === "assistant";
202
+ }
203
+
204
+ function finitePositive(value: number | undefined): number {
205
+ return Number.isFinite(value) && (value ?? 0) > 0 ? value! : 0;
206
+ }
207
+
208
+ function round(value: number, decimals: number): number {
209
+ const factor = 10 ** decimals;
210
+ return Math.round(value * factor) / factor;
211
+ }
212
+
213
+ function aggregateTelemetry(
214
+ turns: readonly TurnTelemetrySnapshot[],
215
+ totalMs: number,
216
+ ): TurnTelemetrySnapshot | undefined {
217
+ if (turns.length === 0) return undefined;
218
+
219
+ const inputTokens = turns.reduce((sum, turn) => sum + turn.inputTokens, 0);
220
+ const outputTokens = turns.reduce((sum, turn) => sum + turn.outputTokens, 0);
221
+ const hasCacheTelemetry = turns.every(
222
+ (turn) => turn.cacheReadTokens !== null && turn.cacheWriteTokens !== null,
223
+ );
224
+ const cacheReadTokens = hasCacheTelemetry
225
+ ? turns.reduce((sum, turn) => sum + turn.cacheReadTokens!, 0)
226
+ : null;
227
+ const cacheWriteTokens = hasCacheTelemetry
228
+ ? turns.reduce((sum, turn) => sum + turn.cacheWriteTokens!, 0)
229
+ : null;
230
+ const costUsd = turns.reduce((sum, turn) => sum + turn.costUsd, 0);
231
+ const generationMs = turns.reduce((sum, turn) => sum + turn.generationMs, 0);
232
+ const tokensPerSecond = outputTokens > 0 && generationMs > 0
233
+ ? round(outputTokens / (generationMs / 1_000), 1)
234
+ : null;
235
+
236
+ return {
237
+ tokensPerSecond,
238
+ ttftMs: turns[0]!.ttftMs,
239
+ totalMs: Math.max(0, totalMs),
240
+ inputTokens,
241
+ outputTokens,
242
+ cacheReadTokens,
243
+ cacheWriteTokens,
244
+ generationMs,
245
+ costUsd,
246
+ };
247
+ }
248
+
249
+ /**
250
+ * 统计一次完整 Agent run。每个 LLM turn 从 turn_start 计到 assistant
251
+ * message_end,工具执行夹在两个 turn 之间,因此不会污染 TPS。
252
+ */
253
+ export class TurnTelemetryTracker {
254
+ private readonly now: () => number;
255
+ private turn: TurnTiming | undefined;
256
+ private agentStartMs: number | null = null;
257
+ private agentTurns: TurnTelemetrySnapshot[] = [];
258
+
259
+ constructor(now: () => number = () => performance.now()) {
260
+ this.now = now;
261
+ }
262
+
263
+ handle(event: TurnTelemetryEvent): TurnTelemetrySnapshot | undefined {
264
+ switch (event.type) {
265
+ case "agent_start":
266
+ if (this.agentStartMs === null) {
267
+ this.agentStartMs = this.now();
268
+ this.agentTurns = [];
269
+ }
270
+ return undefined;
271
+ case "agent_settled":
272
+ return this.endAgent();
273
+ case "turn_start":
274
+ this.startTurn();
275
+ return undefined;
276
+ case "message_start":
277
+ this.startMessage(event.message);
278
+ return undefined;
279
+ case "message_update":
280
+ this.updateMessage(event);
281
+ return undefined;
282
+ case "message_end":
283
+ this.endMessage(event.message);
284
+ return undefined;
285
+ case "turn_end":
286
+ this.endTurnAndCollect();
287
+ return undefined;
288
+ }
289
+ }
290
+
291
+ reset(): void {
292
+ this.turn = undefined;
293
+ this.agentStartMs = null;
294
+ this.agentTurns = [];
295
+ }
296
+
297
+ private startTurn(): void {
298
+ this.turn = {
299
+ startMs: this.now(),
300
+ firstTokenMs: null,
301
+ currentMessage: null,
302
+ messages: [],
303
+ generationMs: 0,
304
+ };
305
+ }
306
+
307
+ private startMessage(message: AgentMessage): void {
308
+ if (!this.turn || !isAssistantMessage(message)) return;
309
+ this.turn.currentMessage = {
310
+ firstOutputMs: null,
311
+ };
312
+ }
313
+
314
+ private updateMessage(event: MessageUpdateEvent): void {
315
+ const streamEvent = event.assistantMessageEvent;
316
+ if (
317
+ streamEvent.type !== "text_delta" &&
318
+ streamEvent.type !== "thinking_delta" &&
319
+ streamEvent.type !== "toolcall_delta"
320
+ ) return;
321
+ if (streamEvent.delta.length === 0) return;
322
+
323
+ const turn = this.turn;
324
+ const current = turn?.currentMessage;
325
+ if (!turn || !current || !isAssistantMessage(event.message)) return;
326
+
327
+ if (current.firstOutputMs === null) {
328
+ const now = this.now();
329
+ current.firstOutputMs = now;
330
+ turn.firstTokenMs ??= now;
331
+ }
332
+ }
333
+
334
+ private endMessage(message: AgentMessage): void {
335
+ const turn = this.turn;
336
+ if (!turn || !isAssistantMessage(message)) return;
337
+
338
+ if (turn.currentMessage) {
339
+ const endMs = this.now();
340
+ if (turn.currentMessage.firstOutputMs !== null) {
341
+ turn.generationMs += Math.max(0, endMs - turn.currentMessage.firstOutputMs);
342
+ }
343
+ if (turn.currentMessage.firstOutputMs === null && finitePositive(message.usage?.output) > 0) {
344
+ turn.firstTokenMs ??= endMs;
345
+ }
346
+ turn.currentMessage = null;
347
+ }
348
+ turn.messages.push(message);
349
+ }
350
+
351
+ private endTurnAndCollect(): void {
352
+ const telemetry = this.endTurn();
353
+ if (telemetry && this.agentStartMs !== null) this.agentTurns.push(telemetry);
354
+ }
355
+
356
+ private endTurn(): TurnTelemetrySnapshot | undefined {
357
+ const turn = this.turn;
358
+ this.turn = undefined;
359
+ if (!turn || turn.firstTokenMs === null || turn.messages.length === 0) return undefined;
360
+
361
+ const inputTokens = turn.messages.reduce(
362
+ (sum, message) => sum + finitePositive(message.usage?.input),
363
+ 0,
364
+ );
365
+ const outputTokens = turn.messages.reduce(
366
+ (sum, message) => sum + finitePositive(message.usage?.output),
367
+ 0,
368
+ );
369
+ const cacheReadTokens = turn.messages.reduce(
370
+ (sum, message) => sum + finitePositive(message.usage?.cacheRead),
371
+ 0,
372
+ );
373
+ const cacheWriteTokens = turn.messages.reduce(
374
+ (sum, message) => sum + finitePositive(message.usage?.cacheWrite),
375
+ 0,
376
+ );
377
+ const costUsd = turn.messages.reduce(
378
+ (sum, message) => sum + finitePositive(message.usage?.cost?.total),
379
+ 0,
380
+ );
381
+ const tokensPerSecond = outputTokens > 0 && turn.generationMs > 0
382
+ ? round(outputTokens / (turn.generationMs / 1_000), 1)
383
+ : null;
384
+
385
+ return {
386
+ tokensPerSecond,
387
+ ttftMs: turn.firstTokenMs - turn.startMs,
388
+ totalMs: this.now() - turn.startMs,
389
+ inputTokens,
390
+ outputTokens,
391
+ cacheReadTokens,
392
+ cacheWriteTokens,
393
+ generationMs: turn.generationMs,
394
+ costUsd,
395
+ };
396
+ }
397
+
398
+ private endAgent(): TurnTelemetrySnapshot | undefined {
399
+ const startMs = this.agentStartMs;
400
+ const turns = this.agentTurns;
401
+ this.reset();
402
+ return startMs === null ? undefined : aggregateTelemetry(turns, this.now() - startMs);
403
+ }
404
+ }
405
+
406
+ function formatTelemetryDuration(ms: number): string {
407
+ return ms < 60_000 ? `${(Math.max(0, ms) / 1_000).toFixed(1)}s` : formatElapsed(ms);
408
+ }
409
+
410
+ function getTtftColor(ttftMs: number): "success" | "accent" | "warning" | "error" {
411
+ if (ttftMs < 3_000) return "success";
412
+ if (ttftMs < 8_000) return "accent";
413
+ if (ttftMs < 15_000) return "warning";
414
+ return "error";
415
+ }
416
+
417
+ function formatTokensPerSecond(value: number): string {
418
+ return round(value, 1).toString();
419
+ }
420
+
421
+ function formatEstimatedCost(costUsd: number): string {
422
+ if (costUsd < 0.0001) return "<$0.0001";
423
+ const decimals = costUsd < 1 ? 4 : 2;
424
+ const amount = costUsd.toFixed(decimals).replace(/\.?0+$/, "");
425
+ return `$${amount}`;
426
+ }
427
+
428
+ export function formatTurnTelemetry(
429
+ telemetry: TurnTelemetrySnapshot,
430
+ theme: Theme,
431
+ glyphs: IconGlyphs = resolveGlyphs("unicode"),
432
+ ): string {
433
+ const parts: string[] = [];
434
+ const speed = telemetry.tokensPerSecond === null
435
+ ? "—"
436
+ : `${formatTokensPerSecond(telemetry.tokensPerSecond)} tok/s`;
437
+ parts.push(theme.fg(
438
+ getTtftColor(telemetry.ttftMs),
439
+ `${glyphs.latency} ${formatTelemetryDuration(telemetry.ttftMs)}`,
440
+ ));
441
+ parts.push(theme.fg(
442
+ telemetry.tokensPerSecond === null ? "muted" : "accent",
443
+ `${glyphs.speed} ${speed}`,
444
+ ));
445
+ const contextParts: string[] = [];
446
+ if (telemetry.inputTokens > 0) {
447
+ contextParts.push(theme.fg("muted", `${glyphs.inputTokens}${formatTokenCount(telemetry.inputTokens)}`));
448
+ }
449
+ if (telemetry.outputTokens > 0) {
450
+ contextParts.push(theme.fg("muted", `${glyphs.outputTokens}${formatTokenCount(telemetry.outputTokens)}`));
451
+ }
452
+ if (
453
+ telemetry.cacheReadTokens !== null
454
+ && telemetry.cacheWriteTokens !== null
455
+ && (telemetry.cacheReadTokens > 0 || telemetry.cacheWriteTokens > 0)
456
+ ) {
457
+ contextParts.push(theme.fg("muted", `R${formatTokenCount(telemetry.cacheReadTokens)}`));
458
+ }
459
+ if (contextParts.length > 0) parts.push(contextParts.join(" "));
460
+ if (telemetry.costUsd > 0) {
461
+ parts.push(theme.fg("warning", formatEstimatedCost(telemetry.costUsd)));
462
+ }
463
+
464
+ const separator = glyphs.cost === "$" ? "|" : "·";
465
+ return parts.join(` ${theme.fg("dim", separator)} `);
466
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Firstpick
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ # 主题资源来源
2
+
3
+ 本目录的 16 份主题来自 `@firstpick/pi-themes-bundle` `0.1.6`,上游仓库为 [Firstp1ck/pi-coding-agent-forge](https://github.com/Firstp1ck/pi-coding-agent-forge/tree/main/pi-package-themes-bundle),固定提交为 `239829d747deae4d9c0f039421ab29440f40f1a2`。许可证见 [LICENSE.pi-themes-bundle](./LICENSE.pi-themes-bundle)。
4
+
5
+ 主题名称和终端调色板保持上游版本。`catppuccin-mocha.json` 与 `matrix.json` 去掉了上游 `export` 中的 `backgroundImage`、`backgroundOverlay`、`backgroundSize`、`backgroundPosition` 和 `backgroundRepeat`。这些字段供上游 WebUI 使用,不属于 Pi `0.84.2` 的 TUI 主题 Schema;删除后不改变终端配色。
6
+
7
+ 本插件直接交付这些主题,用户无需再安装 `@firstpick/pi-themes-bundle`。更新主题前必须重新核对上游固定版本、Pi 当前 Theme Schema 和本文件中的兼容改动,不能跟随浮动分支自动覆盖。
@@ -0,0 +1,80 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "catppuccin-latte",
4
+ "vars": {
5
+ "accent": "#1e66f5",
6
+ "accent2": "#179299",
7
+ "green": "#40a02b",
8
+ "red": "#d20f39",
9
+ "yellow": "#df8e1d",
10
+ "textSoft": "#4c4f69",
11
+ "muted": "#5c5f77",
12
+ "dim": "#6c6f85",
13
+ "borderMuted": "#9ca0b0",
14
+ "bgSelect": "#ccd0da",
15
+ "bgUser": "#eff1f5",
16
+ "bgPending": "#e6e9ef",
17
+ "bgSuccess": "#e6e9ef",
18
+ "bgError": "#e6e9ef",
19
+ "bgCustom": "#ccd0da",
20
+ "cyan": "#179299"
21
+ },
22
+ "colors": {
23
+ "accent": "accent",
24
+ "border": "accent",
25
+ "borderAccent": "accent2",
26
+ "borderMuted": "borderMuted",
27
+ "success": "green",
28
+ "error": "red",
29
+ "warning": "yellow",
30
+ "muted": "muted",
31
+ "dim": "dim",
32
+ "text": "",
33
+ "thinkingText": "muted",
34
+ "selectedBg": "bgSelect",
35
+ "userMessageBg": "bgUser",
36
+ "userMessageText": "textSoft",
37
+ "customMessageBg": "bgCustom",
38
+ "customMessageText": "textSoft",
39
+ "customMessageLabel": "#8839ef",
40
+ "toolPendingBg": "bgPending",
41
+ "toolSuccessBg": "bgSuccess",
42
+ "toolErrorBg": "bgError",
43
+ "toolTitle": "accent",
44
+ "toolOutput": "textSoft",
45
+ "mdHeading": "#dc8a78",
46
+ "mdLink": "accent",
47
+ "mdLinkUrl": "muted",
48
+ "mdCode": "#179299",
49
+ "mdCodeBlock": "#40a02b",
50
+ "mdCodeBlockBorder": "borderMuted",
51
+ "mdQuote": "muted",
52
+ "mdQuoteBorder": "borderMuted",
53
+ "mdHr": "borderMuted",
54
+ "mdListBullet": "#8839ef",
55
+ "toolDiffAdded": "#40a02b",
56
+ "toolDiffRemoved": "#d20f39",
57
+ "toolDiffContext": "muted",
58
+ "syntaxComment": "#7c7f93",
59
+ "syntaxKeyword": "#8839ef",
60
+ "syntaxFunction": "#1e66f5",
61
+ "syntaxVariable": "#4c4f69",
62
+ "syntaxString": "#40a02b",
63
+ "syntaxNumber": "#fe640b",
64
+ "syntaxType": "#179299",
65
+ "syntaxOperator": "#1e66f5",
66
+ "syntaxPunctuation": "#6c6f85",
67
+ "thinkingOff": "borderMuted",
68
+ "thinkingMinimal": "#8c8fa1",
69
+ "thinkingLow": "#1e66f5",
70
+ "thinkingMedium": "#04a5e5",
71
+ "thinkingHigh": "#8839ef",
72
+ "thinkingXhigh": "#d20f39",
73
+ "bashMode": "#40a02b"
74
+ },
75
+ "export": {
76
+ "pageBg": "#dce0e8",
77
+ "cardBg": "#eff1f5",
78
+ "infoBg": "#ccd0da"
79
+ }
80
+ }
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json",
3
+ "name": "catppuccin-mocha",
4
+ "vars": {
5
+ "accent": "#89b4fa",
6
+ "accent2": "#94e2d5",
7
+ "green": "#a6e3a1",
8
+ "red": "#f38ba8",
9
+ "yellow": "#f9e2af",
10
+ "textSoft": "#cdd6f4",
11
+ "muted": "#bac2de",
12
+ "dim": "#a6adc8",
13
+ "borderMuted": "#6c7086",
14
+ "bgSelect": "#313244",
15
+ "bgUser": "#1e1e2e",
16
+ "bgPending": "#181825",
17
+ "bgSuccess": "#313244",
18
+ "bgError": "#313244",
19
+ "bgCustom": "#313244"
20
+ },
21
+ "colors": {
22
+ "accent": "accent",
23
+ "border": "accent",
24
+ "borderAccent": "accent2",
25
+ "borderMuted": "borderMuted",
26
+ "success": "green",
27
+ "error": "red",
28
+ "warning": "yellow",
29
+ "muted": "muted",
30
+ "dim": "dim",
31
+ "text": "",
32
+ "thinkingText": "muted",
33
+ "selectedBg": "bgSelect",
34
+ "userMessageBg": "bgUser",
35
+ "userMessageText": "textSoft",
36
+ "customMessageBg": "bgCustom",
37
+ "customMessageText": "textSoft",
38
+ "customMessageLabel": "#cba6f7",
39
+ "toolPendingBg": "bgPending",
40
+ "toolSuccessBg": "bgSuccess",
41
+ "toolErrorBg": "bgError",
42
+ "toolTitle": "accent",
43
+ "toolOutput": "textSoft",
44
+ "mdHeading": "#f5c2e7",
45
+ "mdLink": "accent",
46
+ "mdLinkUrl": "muted",
47
+ "mdCode": "#94e2d5",
48
+ "mdCodeBlock": "#a6e3a1",
49
+ "mdCodeBlockBorder": "borderMuted",
50
+ "mdQuote": "muted",
51
+ "mdQuoteBorder": "borderMuted",
52
+ "mdHr": "borderMuted",
53
+ "mdListBullet": "#89dceb",
54
+ "toolDiffAdded": "#a6e3a1",
55
+ "toolDiffRemoved": "#f38ba8",
56
+ "toolDiffContext": "muted",
57
+ "syntaxComment": "#9399b2",
58
+ "syntaxKeyword": "#cba6f7",
59
+ "syntaxFunction": "#89b4fa",
60
+ "syntaxVariable": "#f5c2e7",
61
+ "syntaxString": "#a6e3a1",
62
+ "syntaxNumber": "#fab387",
63
+ "syntaxType": "#94e2d5",
64
+ "syntaxOperator": "#cdd6f4",
65
+ "syntaxPunctuation": "#bac2de",
66
+ "thinkingOff": "borderMuted",
67
+ "thinkingMinimal": "#7f849c",
68
+ "thinkingLow": "#74c7ec",
69
+ "thinkingMedium": "#89b4fa",
70
+ "thinkingHigh": "#cba6f7",
71
+ "thinkingXhigh": "#f38ba8",
72
+ "bashMode": "#a6e3a1"
73
+ },
74
+ "export": {
75
+ "pageBg": "#11111b",
76
+ "cardBg": "#1e1e2e",
77
+ "infoBg": "#313244"
78
+ }
79
+ }