pi-cc-extensions 0.9.1 → 0.9.3

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.
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * 自定义底栏:chips + zentui 图标/句子
3
3
  *
4
- * 第一行:model thinking · ██████░░░░ 45%/200k · 󰆼 42% · $0.01 · 用量
5
- * 第二行:cwd in session on  branch (+16 −1) · 其他扩展状态
4
+ * 第一行:model thinking · ██████░░░░ 45%/200k · 󰆼 42% · $0.01 · 用户排到 line1 的插件芯片
5
+ * 第二行:cwd in session on  branch (+16 −1) · 用户排到 line2 的插件芯片
6
+ * 第三行:仅当 line3 有可见插件芯片时出现
6
7
  *
7
8
  * - line1 短芯片,· 分隔;缓存用 zentui 󰆼,费用 success
8
9
  * - line2 用 zentui 句式 in / on + 
@@ -20,13 +21,26 @@ import { join } from "node:path";
20
21
  import { pathToFileURL } from "node:url";
21
22
  import { config } from "../../config/config.ts";
22
23
  import { stripAnsi } from "../../utils/ansi-text.ts";
24
+ import {
25
+ PI_USAGE_KEY,
26
+ isSkippedFooterStatusKey,
27
+ resolveFooterChipLayout,
28
+ visibleFooterPluginTexts,
29
+ type FooterChipLayout,
30
+ } from "./footer-layout.ts";
23
31
 
24
32
  const GIT_REFRESH_INTERVAL_MS = 10_000;
25
33
  const USAGE_REFRESH_INTERVAL_MS = 5 * 60 * 1000;
26
34
  const USAGE_TIMEOUT_MS = 15_000;
27
- // zentui NERD_DEFAULT_ICONS:git / cacheHit
28
- const ICON_GIT = "";
29
- const ICON_CACHE = "󰆼";
35
+ // zentui NERD_DEFAULT_ICONS:git / cacheHit。无 Nerd Font 时留空,只保留文字。
36
+ export const FOOTER_NERD_ICON_GIT = "";
37
+ export const FOOTER_NERD_ICON_CACHE = "󰆼";
38
+
39
+ export function footerGlyphs(nerdIcons: boolean): { git: string; cache: string } {
40
+ return nerdIcons
41
+ ? { git: FOOTER_NERD_ICON_GIT, cache: FOOTER_NERD_ICON_CACHE }
42
+ : { git: "", cache: "" };
43
+ }
30
44
 
31
45
  type GitStats = { add: number; del: number };
32
46
 
@@ -54,12 +68,6 @@ export function parseGitStats(stdout: string): GitStats {
54
68
  return { add, del };
55
69
  }
56
70
 
57
- export function classifyStatus(key: string): "usage" | "skip" | "other" {
58
- if (key === "model") return "skip"; // line1 已有供应商/模型
59
- if (/usage|quota|balance/i.test(key)) return "usage";
60
- return "other";
61
- }
62
-
63
71
  export function formatXaiFooterChip(report: XaiFooterReport): string | undefined {
64
72
  const included = report.buckets?.find((b) => b.id === "included-allowance");
65
73
  if (included?.unit === "percent" && typeof included.used === "number") {
@@ -87,14 +95,44 @@ export function formatXaiFooterChip(report: XaiFooterReport): string | undefined
87
95
  return undefined;
88
96
  }
89
97
 
90
- export function pickFooterUsageText(
91
- pluginUsage: string | undefined,
92
- localChip: string | undefined,
93
- ): string | undefined {
94
- const plugin = pluginUsage?.trim();
95
- if (plugin && !/^checking$/i.test(plugin)) return plugin;
96
- const local = localChip?.trim();
97
- return local || undefined;
98
+ const cachedExtensionStatuses = new Map<string, string>();
99
+ let cachedLocalUsageText = "";
100
+
101
+ /** 面板用:当前 setStatus 文案 + 本包 pi-usage(无文案时为空字符串)。 */
102
+ export function getFooterStatusSnapshot(): Map<string, string> {
103
+ const out = new Map<string, string>();
104
+ for (const [key, text] of cachedExtensionStatuses) {
105
+ if (isSkippedFooterStatusKey(key)) continue;
106
+ out.set(key, text);
107
+ }
108
+ out.set(PI_USAGE_KEY, cachedLocalUsageText);
109
+ return out;
110
+ }
111
+
112
+ function rememberExtensionStatuses(entries: Iterable<[string, string]>): void {
113
+ cachedExtensionStatuses.clear();
114
+ for (const [key, raw] of entries) {
115
+ if (isSkippedFooterStatusKey(key)) continue;
116
+ const text = stripAnsi(raw.replace(/[\r\n\t]+/g, " ").trim());
117
+ if (text) cachedExtensionStatuses.set(key, text);
118
+ }
119
+ }
120
+
121
+ function pluginTextsForRender(localUsageChip: string): Map<string, string> {
122
+ cachedLocalUsageText = stripAnsi(localUsageChip.replace(/[\r\n\t]+/g, " ").trim());
123
+ const texts = new Map<string, string>(cachedExtensionStatuses);
124
+ if (cachedLocalUsageText) texts.set(PI_USAGE_KEY, cachedLocalUsageText);
125
+ else texts.delete(PI_USAGE_KEY);
126
+ return texts;
127
+ }
128
+
129
+ function layoutFromConfig(): FooterChipLayout {
130
+ return {
131
+ footerHiddenKeys: config.footerHiddenKeys,
132
+ footerLine1Keys: config.footerLine1Keys,
133
+ footerLine2Keys: config.footerLine2Keys,
134
+ footerLine3Keys: config.footerLine3Keys,
135
+ };
98
136
  }
99
137
 
100
138
  let piUsageMod: any | null | undefined;
@@ -317,26 +355,33 @@ const createCustomFooterFactory =
317
355
  const thinkingLevelStr = ctx.thinkingLevel || "off";
318
356
  const thinkingColor = theme.getThinkingBorderColor(thinkingLevelStr);
319
357
 
320
- const otherStatuses: string[] = [];
321
- const usageStatuses: string[] = [];
322
- for (const [key, raw] of Array.from(
358
+ rememberExtensionStatuses(
323
359
  footerData.getExtensionStatuses().entries() as Iterable<[string, string]>,
324
- ).sort(([a], [b]) => a.localeCompare(b))) {
325
- const t = raw.replace(/[\r\n\t]+/g, " ").trim();
326
- if (!t) continue;
327
- const kind = classifyStatus(key);
328
- if (kind === "skip") continue;
329
- if (kind === "usage") {
330
- usageStatuses.push(stripAnsi(t));
331
- } else {
332
- otherStatuses.push(theme.fg("dim", stripAnsi(t)));
333
- }
334
- }
335
-
336
- const usageText = pickFooterUsageText(usageStatuses[0], localUsageChip);
360
+ );
361
+ const pluginTexts = pluginTextsForRender(localUsageChip);
362
+ const layout = resolveFooterChipLayout(layoutFromConfig(), [...pluginTexts.keys()]);
363
+ const dimPlugin = (text: string) => colorUsageChip(theme, text);
364
+ const line1Plugins = visibleFooterPluginTexts(
365
+ layout.footerLine1Keys,
366
+ layout.footerHiddenKeys,
367
+ pluginTexts,
368
+ ).map(dimPlugin);
369
+ const line2Plugins = visibleFooterPluginTexts(
370
+ layout.footerLine2Keys,
371
+ layout.footerHiddenKeys,
372
+ pluginTexts,
373
+ ).map(dimPlugin);
374
+ const line3Plugins = visibleFooterPluginTexts(
375
+ layout.footerLine3Keys,
376
+ layout.footerHiddenKeys,
377
+ pluginTexts,
378
+ ).map(dimPlugin);
337
379
 
338
380
  const pctLabel = percent === null || percent === undefined ? "?" : `${Math.floor(percent)}%`;
339
381
  const cachePct = getCachePct();
382
+ const glyphs = footerGlyphs(config.footerNerdIcons);
383
+ const cacheLabel =
384
+ cachePct > 0 ? `${glyphs.cache ? `${glyphs.cache} ` : ""}${Math.floor(cachePct)}%` : "";
340
385
  const costChip =
341
386
  cost || usingSubscription
342
387
  ? theme.fg("dim", `$${cost.toFixed(2)}`) +
@@ -346,9 +391,9 @@ const createCustomFooterFactory =
346
391
  theme.fg("accent", modelLabel),
347
392
  model?.reasoning ? thinkingColor(thinkingLevelStr) : "",
348
393
  barGauge(percent ?? 0) + theme.fg("dim", ` ${pctLabel}/${fmt(contextWindow)}`),
349
- cachePct > 0 ? theme.fg("dim", `${ICON_CACHE} ${Math.floor(cachePct)}%`) : "",
394
+ cacheLabel ? theme.fg("dim", cacheLabel) : "",
350
395
  costChip,
351
- usageText ? colorUsageChip(theme, usageText) : "",
396
+ ...line1Plugins,
352
397
  ]);
353
398
 
354
399
  const gitColor = theme.getThinkingBorderColor("medium");
@@ -367,11 +412,17 @@ const createCustomFooterFactory =
367
412
  theme.fg("error", `−${gitStats.del}`) +
368
413
  theme.fg("dim", ")")
369
414
  : "";
370
- place += theme.fg("muted", " on ") + gitColor(`${ICON_GIT} ${branch}`) + stats;
415
+ const gitLabel = glyphs.git ? `${glyphs.git} ${branch}` : branch;
416
+ place += theme.fg("muted", " on ") + gitColor(gitLabel) + stats;
371
417
  }
372
- const line2 = joinChips([place, ...otherStatuses]);
373
-
374
- return [truncateToWidth(line1, width), ...(line2 ? [truncateToWidth(line2, width)] : [])];
418
+ const line2 = joinChips([place, ...line2Plugins]);
419
+ const line3 = joinChips(line3Plugins);
420
+
421
+ return [
422
+ truncateToWidth(line1, width),
423
+ ...(line2 ? [truncateToWidth(line2, width)] : []),
424
+ ...(line3 ? [truncateToWidth(line3, width)] : []),
425
+ ];
375
426
  };
376
427
 
377
428
  return {
@@ -241,12 +241,16 @@ function createCcstyleTool(
241
241
  const isPending =
242
242
  visualState === "pending" ||
243
243
  (!visualState && (context?.isPartial || context?.executionStarted));
244
- if (isPending && context?.executionStarted) scheduleAnimation(context);
245
- const rawIcon = isPending ? pendingIcon(toolName) : settledIcon(toolName, visualState);
246
- const icon =
244
+ const animating = isPending && Boolean(context?.executionStarted);
245
+ if (animating) scheduleAnimation(context, { light: true });
246
+ const settledRawIcon = isPending ? "" : settledIcon(toolName, visualState);
247
+ const settledIconStyled =
247
248
  visualState === "success"
248
- ? `${BRIGHT_GREEN}${rawIcon}${ANSI_FG_RESET}`
249
- : theme.fg(toolIconColor(context), rawIcon);
249
+ ? `${BRIGHT_GREEN}${settledRawIcon}${ANSI_FG_RESET}`
250
+ : theme.fg(toolIconColor(context), settledRawIcon);
251
+ // 逐帧 spinner:帧在 render() 内取,动画定时器只需 requestRender,
252
+ // 不再每次 tick 走 updateDisplay 把整张卡重建一遍。
253
+ const pendingIconStyled = () => theme.fg(toolIconColor(context), pendingIcon(toolName));
250
254
  const summary = toolCallSummary(toolName, args, {
251
255
  title: label === toolName ? humanizeToolLabel(label) : label,
252
256
  variant: "default",
@@ -270,10 +274,16 @@ function createCcstyleTool(
270
274
  const extraStyled = writeStatsStyled || theme.fg("dim", summary.detail);
271
275
  let cachedWidth: number | undefined;
272
276
  let cachedLine: string | undefined;
277
+ let cachedIcon: string | undefined;
273
278
  const expanded = Boolean(context?.expanded);
274
279
  return {
275
280
  render(width: number) {
276
- if (cachedLine !== undefined && cachedWidth === width) return [cachedLine];
281
+ // 轻量 tick 只会 requestRender,靠这里续期,动画才能自维持。
282
+ if (animating) scheduleAnimation(context, { light: true });
283
+ const icon = isPending ? pendingIconStyled() : settledIconStyled;
284
+ if (cachedLine !== undefined && cachedWidth === width && cachedIcon === icon) {
285
+ return [cachedLine];
286
+ }
277
287
  const viewportWidth = toolViewportWidth(width);
278
288
  // 展开态贴左(外层 Box 已 pad 1);折叠 self-shell 保留 1 格前导空格
279
289
  const lead = expanded ? "" : " ";
@@ -283,11 +293,15 @@ function createCcstyleTool(
283
293
  );
284
294
  const mainWidth = Math.max(0, callWidth - visibleWidth(extraText));
285
295
  cachedWidth = width;
296
+ cachedIcon = icon;
286
297
  // 路径按最终可用宽度中间截断,避免整行二次截断隐藏文件名。
287
298
  cachedLine = `${lead}${icon} ${theme.fg("toolTitle", fitToolCallSummary(summary, mainWidth))}${extraStyled}`;
288
299
  return [truncateToWidth(cachedLine, viewportWidth, "")];
289
300
  },
290
- invalidate() {},
301
+ invalidate() {
302
+ cachedLine = undefined;
303
+ cachedIcon = undefined;
304
+ },
291
305
  };
292
306
  },
293
307
  renderResult(result: any, options: any, theme: any, context: any) {
@@ -7,6 +7,7 @@ import {
7
7
  type Component,
8
8
  } from "@earendil-works/pi-tui";
9
9
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
10
+ import { getToolMouseTui } from "../mouse/scroll.ts";
10
11
  import { isToolTuiFullscreen, showMoreHintText } from "./show-more-hint.ts";
11
12
  import { stripAnsi, stripBackgroundAnsi, stripLeadingStatusIcon } from "../../utils/ansi-text.ts";
12
13
  import { walkComponentTree } from "../../utils/component-tree.ts";
@@ -89,18 +90,38 @@ function scheduleGroupAnimation(patch: Patch): void {
89
90
  patch.animationTimer = setTimeout(() => {
90
91
  patch.animationTimer = null;
91
92
  if (!patch.active) return;
93
+ let needsRender = false;
92
94
  for (const group of patch.groups) {
93
95
  if (
94
96
  (group.children as any[]).some(
95
97
  (tool) => tool?.executionStarted && status(tool) === "pending",
96
98
  )
97
- )
99
+ ) {
98
100
  group.invalidate();
101
+ // 分组卡不是 ToolExecutionComponent:它的 invalidate() 不会请求渲染,
102
+ // 不补这一步 spinner 只在别人渲染时才跳帧(并行工具下表现为卡顿/冻结)。
103
+ needsRender = true;
104
+ }
99
105
  }
106
+ if (needsRender) requestAnimationRender(patch);
100
107
  }, TOOL_LOADING_INTERVAL_MS);
101
108
  patch.animationTimer.unref?.();
102
109
  }
103
110
 
111
+ /** 借用子工具卡的 ui 请求一帧;子卡缺失时回退到扩展持有的 TUI 槽。 */
112
+ function requestAnimationRender(patch: Patch): void {
113
+ for (const group of patch.groups) {
114
+ const ui = (group.children as any[]).find(
115
+ (tool) => typeof tool?.ui?.requestRender === "function",
116
+ )?.ui;
117
+ if (ui) {
118
+ ui.requestRender();
119
+ return;
120
+ }
121
+ }
122
+ getToolMouseTui()?.requestRender?.();
123
+ }
124
+
104
125
  function visibleLines(lines: string[]): string[] {
105
126
  return lines.filter((line) => stripAnsi(line).trim());
106
127
  }
@@ -7,11 +7,18 @@ function clip(value: unknown): string {
7
7
  return oneLine(value, config.inputClip);
8
8
  }
9
9
 
10
+ /** 品牌大小写固定写法;humanize 推导不出来的工具名写在这里。 */
11
+ const TOOL_LABEL_OVERRIDES: Readonly<Record<string, string>> = {
12
+ powershell: "PowerShell",
13
+ };
14
+
10
15
  /**
11
16
  * 工具名/标签人性化:与 default-mode 的 humanizeToolLabel、grouping 的 humanizeToolName
12
17
  * 逐字相同,收敛为一个共享实现。
13
18
  */
14
19
  export function humanizeToolLabel(label: string): string {
20
+ const brand = TOOL_LABEL_OVERRIDES[label.toLowerCase()];
21
+ if (brand) return brand;
15
22
  return label
16
23
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
17
24
  .replace(/[_-]+/g, " ")
@@ -134,11 +141,84 @@ function pathSummary(
134
141
  };
135
142
  }
136
143
 
144
+ /** 入参是 shell 命令的工具。 */
145
+ const SHELL_TOOL_NAMES = new Set(["bash", "powershell", "pwsh", "sh", "zsh", "cmd"]);
146
+
147
+ /** 内置检索工具:pattern 缺失(无效调用)时保留 "…" 占位。 */
148
+ const SEARCH_TOOL_NAMES = new Set(["grep", "find"]);
149
+
150
+ /**
151
+ * 通用摘要字段优先级:default 与 grouping 共用同一份取值链。
152
+ * 两处曾各自维护,grouping 因此漏掉 command(powershell)和数组型入参;
153
+ * 路径不在链上,由 pathSummary 兜底,以便按 cwd 相对化并中间截断。
154
+ */
155
+ const GENERIC_SUMMARY_FIELDS = [
156
+ "agent_id",
157
+ "command",
158
+ "query",
159
+ "queries",
160
+ "claim",
161
+ "question",
162
+ "questions",
163
+ "url",
164
+ "urls",
165
+ "responseId",
166
+ "findText",
167
+ "name",
168
+ "tool_use_id",
169
+ "toolCallId",
170
+ "id",
171
+ "subject",
172
+ "taskId",
173
+ "task_id",
174
+ "message",
175
+ "description",
176
+ "prompt",
177
+ ] as const;
178
+
179
+ /** 通用字段链取值:字符串直接用;数组取首个可展示项并标注剩余条数。 */
180
+ type SummaryValue = { text: string; more: number };
181
+
182
+ function arrayItemText(item: unknown): string | undefined {
183
+ if (typeof item === "string" && item) return item;
184
+ if (!item || typeof item !== "object") return undefined;
185
+ for (const key of ["query", "url", "question", "header", "name"] as const) {
186
+ const text = (item as Record<string, unknown>)[key];
187
+ if (typeof text === "string" && text) return text;
188
+ }
189
+ return undefined;
190
+ }
191
+
192
+ function summaryFieldValue(value: unknown): SummaryValue | undefined {
193
+ if (typeof value === "string" && value) return { text: value, more: 0 };
194
+ if (!Array.isArray(value)) return undefined;
195
+ let first: string | undefined;
196
+ let count = 0;
197
+ for (const item of value) {
198
+ const text = arrayItemText(item);
199
+ if (!text) continue;
200
+ count += 1;
201
+ first ??= text;
202
+ }
203
+ return first === undefined ? undefined : { text: first, more: count - 1 };
204
+ }
205
+
206
+ /** 通用字段链取值;无可用字段时返回 undefined,交给调用方继续降级。 */
207
+ function genericSummary(title: string, args: any): ToolCallSummary | undefined {
208
+ for (const key of GENERIC_SUMMARY_FIELDS) {
209
+ const found = summaryFieldValue(args[key]);
210
+ if (!found) continue;
211
+ // (+N) 追加在截断之后,避免首个元素过长时把条数挤没
212
+ const more = found.more > 0 ? ` (+${found.more})` : "";
213
+ return { main: `${title} ${clip(found.text)}${more}`, detail: "" };
214
+ }
215
+ return undefined;
216
+ }
217
+
137
218
  /**
138
219
  * 单工具调用摘要({ main, detail })。
139
220
  *
140
- * default-mode 与 grouping 共用;opts.variant 保留两处各自逐字一致的输出,
141
- * 不改动任何现有渲染字符串。
221
+ * 两个变体共用同一份取值链,只有 read/agent/skill/task 等具名分支存在必要差异。
142
222
  */
143
223
  export function toolCallSummary(
144
224
  toolName: string,
@@ -216,58 +296,24 @@ export function toolCallSummary(
216
296
  detail,
217
297
  };
218
298
  }
219
- if (variant === "grouping") {
220
- if (toolName === "bash") return { main: `Bash ${clip(args.command || "...")}`, detail: "" };
221
- if (toolName === "grep") {
222
- const pattern = clip(args.pattern || "...");
223
- return {
224
- main: `Grep ${JSON.stringify(pattern)}${args.path ? ` in ${clip(args.path)}` : ""}`,
225
- detail: "",
226
- };
227
- }
228
- if (toolName === "find") {
229
- const pattern = clip(args.pattern || "...");
230
- return {
231
- main: `Find ${JSON.stringify(pattern)}${args.path ? ` in ${clip(args.path)}` : ""}`,
232
- detail: "",
233
- };
234
- }
299
+ if (variant === "grouping" && SHELL_TOOL_NAMES.has(name)) {
300
+ // 分组行缺 command 时保留占位,避免与单工具卡一样只剩标题
301
+ return { main: value("...", "command"), detail: "" };
235
302
  }
236
- if (variant === "default") {
237
- const preferredPath = args.path ?? args.file_path;
238
- if (typeof preferredPath === "string" && preferredPath) {
239
- return pathSummary(title, preferredPath, opts.cwd);
240
- }
241
- const preferred =
242
- args.command ??
243
- args.query ??
244
- args.question ??
245
- args.pattern ??
246
- args.url ??
247
- args.name ??
248
- args.tool_use_id ??
249
- args.toolCallId ??
250
- args.id ??
251
- args.message;
252
- return {
253
- main:
254
- preferred !== undefined && preferred !== null && typeof preferred !== "object"
255
- ? `${title} ${clip(preferred)}`
256
- : title,
257
- detail: "",
258
- };
303
+
304
+ // 检索类:pattern 是正文,path 只是范围;两个变体同格式
305
+ if (typeof args.pattern === "string" || SEARCH_TOOL_NAMES.has(name)) {
306
+ const pattern = clip(args.pattern || "...");
307
+ const scope = typeof args.path === "string" && args.path ? ` in ${clip(args.path)}` : "";
308
+ return { main: `${title} ${JSON.stringify(pattern)}${scope}`, detail: "" };
259
309
  }
260
- const preferred =
261
- args.agent_id ??
262
- args.path ??
263
- args.file_path ??
264
- args.url ??
265
- args.description ??
266
- args.query ??
267
- args.name ??
268
- args.prompt;
269
- return {
270
- main: `${title}${preferred === undefined ? "" : ` ${clip(preferred)}`}`,
271
- detail: "",
272
- };
310
+
311
+ const generic = genericSummary(title, args);
312
+ if (generic) return generic;
313
+
314
+ const preferredPath = args.path ?? args.file_path;
315
+ if (typeof preferredPath === "string" && preferredPath) {
316
+ return pathSummary(title, preferredPath, opts.cwd);
317
+ }
318
+ return { main: title, detail: "" };
273
319
  }
@@ -3,6 +3,7 @@ import { inspect } from "node:util";
3
3
  import { config } from "../../config/config.ts";
4
4
  import { showMoreHintText } from "./show-more-hint.ts";
5
5
  import { TOOL_LOADING_INTERVAL_MS, toolLoadingIcon } from "../../utils/tool-loading-icon.ts";
6
+ import { getToolMouseTui } from "../mouse/scroll.ts";
6
7
  import { sanitizeToolResultText } from "../../utils/tool-result-sanitize.ts";
7
8
 
8
9
  const TOOL_VIEWPORT_WIDTH_RATIO = 0.8;
@@ -70,6 +71,7 @@ const activeAnimationContexts = new Set<any>();
70
71
  let sharedAnimationTimer: ReturnType<typeof setTimeout> | null = null;
71
72
 
72
73
  function clearAnimation(context: any) {
74
+ if (context?.state) context.state.ccstyleAnimationLight = false;
73
75
  if (!context?.state?.ccstyleAnimationScheduled) return;
74
76
  context.state.ccstyleAnimationScheduled = false;
75
77
  activeAnimationContexts.delete(context);
@@ -82,6 +84,7 @@ function clearAnimation(context: any) {
82
84
  export function clearAllAnimations() {
83
85
  for (const ctx of activeAnimationContexts) {
84
86
  ctx.state.ccstyleAnimationScheduled = false;
87
+ ctx.state.ccstyleAnimationLight = false;
85
88
  }
86
89
  activeAnimationContexts.clear();
87
90
  if (sharedAnimationTimer) {
@@ -90,8 +93,15 @@ export function clearAllAnimations() {
90
93
  }
91
94
  }
92
95
 
93
- export function scheduleAnimation(context: any, intervalMs = TOOL_LOADING_INTERVAL_MS) {
96
+ export function scheduleAnimation(
97
+ context: any,
98
+ options: { light?: boolean; intervalMs?: number } = {},
99
+ ) {
94
100
  const state = (context.state ??= {});
101
+ // light:调用方自己在 render() 内重取 loading 帧,定时器只需请求重绘;
102
+ // 否则定时器走 context.invalidate()(compact 摘要等靠它重建)。
103
+ // 每次调用都按本次 options 写入,避免上次 light 残留后非 light 调用仍跳过 invalidate。
104
+ state.ccstyleAnimationLight = Boolean(options.light);
95
105
  if (state.ccstyleAnimationScheduled) return;
96
106
  state.ccstyleAnimationScheduled = true;
97
107
  activeAnimationContexts.add(context);
@@ -100,11 +110,16 @@ export function scheduleAnimation(context: any, intervalMs = TOOL_LOADING_INTERV
100
110
  sharedAnimationTimer = null;
101
111
  const contexts = Array.from(activeAnimationContexts);
102
112
  activeAnimationContexts.clear();
113
+ const tui = getToolMouseTui();
114
+ const canRequestRender = typeof tui?.requestRender === "function";
115
+ let lightFrames = false;
103
116
  for (const ctx of contexts) {
104
117
  ctx.state.ccstyleAnimationScheduled = false;
105
- ctx.invalidate?.();
118
+ if (ctx.state.ccstyleAnimationLight && canRequestRender) lightFrames = true;
119
+ else ctx.invalidate?.();
106
120
  }
107
- }, intervalMs);
121
+ if (lightFrames) tui.requestRender();
122
+ }, options.intervalMs ?? TOOL_LOADING_INTERVAL_MS);
108
123
  }
109
124
  }
110
125
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "pi-cc-extensions",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "license": "MIT",
5
- "description": "A Pi productivity suite with Claude Code-style UI, context inspection, and agent/session references",
5
+ "description": "Claude Code-style TUI output with some personal touches and handy utilities.",
6
6
  "type": "module",
7
7
  "keywords": [
8
8
  "pi-package",
@@ -21,7 +21,6 @@
21
21
  "files": [
22
22
  "extensions",
23
23
  "assets/readme",
24
- "assets/video",
25
24
  "themes",
26
25
  "README.md",
27
26
  "README.en.md"
@@ -34,7 +33,7 @@
34
33
  "./themes/cc-dark.json",
35
34
  "./themes/cc-light.json"
36
35
  ],
37
- "image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/preview.png",
36
+ "image": "https://raw.githubusercontent.com/minuque/pi-cc-extensions/main/assets/readme/preview.webp",
38
37
  "video": "https://github.com/user-attachments/assets/6c858000-fdad-43f9-957f-4d0278648498"
39
38
  },
40
39
  "scripts": {