pi-cc-extensions 0.8.51 → 0.8.52

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.
@@ -13,6 +13,13 @@ import {
13
13
  } from "@earendil-works/pi-coding-agent";
14
14
  import { getKeybindings } from "@earendil-works/pi-tui";
15
15
  import { Markdown, Spacer, Text, type Component } from "@earendil-works/pi-tui";
16
+ import {
17
+ animateCompactThinkingText,
18
+ formatThoughtDuration,
19
+ styleCompactThinkingText,
20
+ } from "../renderer/compact-mode.ts";
21
+ // 保持导出兼容:渲染函数已并入 renderer/compact-mode.ts,这里 re-export。
22
+ export { animateCompactThinkingText, formatThoughtDuration, styleCompactThinkingText };
16
23
 
17
24
  // pi-tui 类型声明中 TUI 的 re-export 解析失败,本地用最小结构化类型(只用到 requestRender)。
18
25
  type RenderTui = { requestRender(force?: boolean): void };
@@ -28,6 +35,18 @@ export type CompactThinkingConfig = {
28
35
 
29
36
  export type CompactThinkingController = {
30
37
  updateConfig(next: CompactThinkingConfig): void;
38
+ /**
39
+ * 只读查询:某条 assistant message 的可见思考总时长(ms)。
40
+ * 供 compact 渲染层生成 `Thought for 8s` 摘要,不建立第二套计时器。
41
+ * 无记录且无进行中思考时返回 undefined。
42
+ */
43
+ getMessageThinkingDurationMs?(messageTimestamp: number): number | undefined;
44
+ /** 当前消息是否仍处于 compact-thinking 的活动思考态。 */
45
+ isMessageThinkingActive?(messageTimestamp: number): boolean;
46
+ /** 复用 compact-thinking 配置的动画帧,不建立独立动画计时器。 */
47
+ getThinkingAnimationFrame?(): number;
48
+ /** compact 摘要保持 loading 时复用 compact-thinking 的动画循环。 */
49
+ setCompactSummaryActive?(active: boolean): void;
31
50
  };
32
51
 
33
52
  type DurationEntryData = {
@@ -86,6 +105,16 @@ const config: CompactThinkingConfig = {
86
105
  };
87
106
 
88
107
  const DURATION_ENTRY_TYPE = "compact-thinking-duration";
108
+ const COMPACT_THINKING_PATCH_KEY = Symbol.for("pi.ccstyle.compact-thinking-update");
109
+ const PROTOTYPE_ORIGINAL_KEY = Symbol.for("pi.ccstyle.prototype-original");
110
+
111
+ /** 当前激活 session 的只读查询委托(compact 渲染层使用)。 */
112
+ type ThinkingDurationQuery = (messageTimestamp: number) => number | undefined;
113
+ type ThinkingActiveQuery = (messageTimestamp: number) => boolean;
114
+ let activeThinkingQuery: ThinkingDurationQuery | undefined;
115
+ let activeThinkingStateQuery: ThinkingActiveQuery | undefined;
116
+ let activeThinkingAnimationFrameQuery: (() => number) | undefined;
117
+ let activeCompactSummarySetter: ((active: boolean) => void) | undefined;
89
118
 
90
119
  function restoreDurationEntries(
91
120
  entries: Array<{ type: string; customType?: string; data?: unknown }>,
@@ -124,18 +153,6 @@ function restoreDurationEntries(
124
153
  }
125
154
  }
126
155
 
127
- function formatThoughtDuration(durationMs: number) {
128
- if (durationMs < 1_000) {
129
- return `${Math.max(1, Math.round(durationMs))}ms`;
130
- }
131
-
132
- const totalSeconds = Math.max(1, Math.round(durationMs / 1_000));
133
- const minutes = Math.floor(totalSeconds / 60);
134
- const seconds = totalSeconds % 60;
135
- if (minutes === 0) return `${seconds}s`;
136
- return seconds === 0 ? `${minutes}m` : `${minutes}m ${seconds}s`;
137
- }
138
-
139
156
  function getThinkingToggleHint() {
140
157
  const keys = getKeybindings().getKeys("app.thinking.toggle");
141
158
  return keys.length > 0 ? `${keys.join("/")} to expand` : undefined;
@@ -253,6 +270,21 @@ function compactThinking(pi: ExtensionAPI) {
253
270
  const prototype = AssistantMessageComponent.prototype as PatchedPrototype;
254
271
  const originalUpdateContent = prototype.updateContent;
255
272
 
273
+ activeThinkingStateQuery = (messageTimestamp) =>
274
+ activeThinking?.messageTimestamp === messageTimestamp;
275
+ activeThinkingAnimationFrameQuery = () => animationFrame;
276
+ activeThinkingQuery = (messageTimestamp) => {
277
+ let total = 0;
278
+ const durations = completedDurations.get(messageTimestamp);
279
+ if (durations) {
280
+ for (const duration of durations.values()) total += duration;
281
+ }
282
+ if (activeThinking?.messageTimestamp === messageTimestamp) {
283
+ total += Math.max(1, Date.now() - activeThinking.startedAt);
284
+ }
285
+ return total > 0 ? total : undefined;
286
+ };
287
+
256
288
  const completedDurations = new Map<number, Map<number, number>>();
257
289
  const renderedComponents = new Set<AssistantMessageComponentLike>();
258
290
  const streamingComponents = new Set<AssistantMessageComponentLike>();
@@ -263,6 +295,7 @@ function compactThinking(pi: ExtensionAPI) {
263
295
  let latestComponentTimestamp: number | undefined;
264
296
  let animationTimer: ReturnType<typeof setInterval> | undefined;
265
297
  let animationFrame = 0;
298
+ let compactSummaryActive = false;
266
299
  let patchInstalled = true;
267
300
 
268
301
  function refreshRenderedComponents() {
@@ -274,35 +307,20 @@ function compactThinking(pi: ExtensionAPI) {
274
307
  }
275
308
 
276
309
  function thinkingStyle(text: string) {
277
- return activeTheme ? activeTheme.italic(activeTheme.fg("thinkingText", text)) : text;
310
+ return styleCompactThinkingText(text, activeTheme);
278
311
  }
279
312
 
280
313
  function summaryTitleStyle(text: string) {
281
- return activeTheme
282
- ? activeTheme.italic(activeTheme.bold(activeTheme.fg("thinkingText", text)))
283
- : text;
314
+ return styleCompactThinkingText(text, activeTheme, true);
284
315
  }
285
316
 
286
317
  function animatedText(text: string, baseStyle: (value: string) => string, animate: boolean) {
287
- if (!animate || !activeTheme) return baseStyle(text);
288
-
289
- const characters = Array.from(text);
290
- if (characters.length === 0) return "";
291
- const highlightWidth = Math.max(1, Math.min(5, Math.ceil(characters.length * 0.28)));
292
- const start = (animationFrame % (characters.length + highlightWidth)) - highlightWidth;
293
- const end = start + highlightWidth;
294
- const before = characters.slice(0, Math.max(0, start)).join("");
295
- const highlighted = characters
296
- .slice(Math.max(0, start), Math.min(characters.length, end))
297
- .join("");
298
- const after = characters.slice(Math.max(0, end)).join("");
299
-
300
- return (
301
- baseStyle(before) +
302
- (highlighted
303
- ? activeTheme.italic(activeTheme.bold(activeTheme.fg("text", highlighted)))
304
- : "") +
305
- baseStyle(after)
318
+ if (!animate) return baseStyle(text);
319
+ return animateCompactThinkingText(
320
+ text,
321
+ activeTheme,
322
+ animationFrame,
323
+ baseStyle === summaryTitleStyle,
306
324
  );
307
325
  }
308
326
 
@@ -506,6 +524,33 @@ function compactThinking(pi: ExtensionAPI) {
506
524
  }
507
525
  }
508
526
  };
527
+ const installedUpdateContent = prototype.updateContent;
528
+ (installedUpdateContent as any)[COMPACT_THINKING_PATCH_KEY] = true;
529
+
530
+ function ensureAnimationTimer() {
531
+ if (animationTimer) return;
532
+ animationTimer = setInterval(() => {
533
+ animationFrame++;
534
+ for (const component of streamingComponents) {
535
+ const self = component as unknown as AssistantInternals;
536
+ if (self.lastMessage) self.updateContent(self.lastMessage);
537
+ }
538
+ activeTui?.requestRender();
539
+ }, config.animationIntervalMs);
540
+ }
541
+
542
+ function stopAnimationTimerIfIdle() {
543
+ if (activeThinking || compactSummaryActive || !animationTimer) return;
544
+ clearInterval(animationTimer);
545
+ animationTimer = undefined;
546
+ }
547
+
548
+ activeCompactSummarySetter = (active) => {
549
+ compactSummaryActive = active;
550
+ if (active) ensureAnimationTimer();
551
+ else stopAnimationTimerIfIdle();
552
+ activeTui?.requestRender();
553
+ };
509
554
 
510
555
  function startThinking(message: AssistantMessage, contentIndex: number) {
511
556
  activeThinking = {
@@ -528,14 +573,8 @@ function compactThinking(pi: ExtensionAPI) {
528
573
  }
529
574
 
530
575
  if (animationTimer) clearInterval(animationTimer);
531
- animationTimer = setInterval(() => {
532
- animationFrame++;
533
- for (const component of streamingComponents) {
534
- const self = component as unknown as AssistantInternals;
535
- if (self.lastMessage) self.updateContent(self.lastMessage);
536
- }
537
- activeTui?.requestRender();
538
- }, config.animationIntervalMs);
576
+ animationTimer = undefined;
577
+ ensureAnimationTimer();
539
578
  }
540
579
 
541
580
  function finishThinking() {
@@ -555,8 +594,7 @@ function compactThinking(pi: ExtensionAPI) {
555
594
  } as DurationEntryData);
556
595
 
557
596
  activeThinking = undefined;
558
- if (animationTimer) clearInterval(animationTimer);
559
- animationTimer = undefined;
597
+ stopAnimationTimerIfIdle();
560
598
 
561
599
  const components = [...streamingComponents];
562
600
  streamingComponents.clear();
@@ -589,7 +627,7 @@ function compactThinking(pi: ExtensionAPI) {
589
627
  content?.type === "toolCall" &&
590
628
  (content.name === "Agent" ||
591
629
  content.name === "Agents" ||
592
- content.args?.subagent_type != null),
630
+ content.arguments?.subagent_type != null),
593
631
  )
594
632
  );
595
633
  }
@@ -633,7 +671,9 @@ function compactThinking(pi: ExtensionAPI) {
633
671
  // OpenAI-compatible providers may not close reasoning until the response ends.
634
672
  pi.on("tool_execution_start", (event: any) => {
635
673
  if (event.toolName === "Agent" || event.toolName === "Agents") {
636
- resumeAgentThinking(latestComponent?.lastMessage ?? undefined);
674
+ const lastMessage = (latestComponent as unknown as { lastMessage?: AssistantMessage })
675
+ ?.lastMessage;
676
+ resumeAgentThinking(lastMessage);
637
677
  } else {
638
678
  finishThinking();
639
679
  }
@@ -674,6 +714,11 @@ function compactThinking(pi: ExtensionAPI) {
674
714
 
675
715
  pi.on("session_shutdown", (_event, ctx) => {
676
716
  finishThinking();
717
+ activeThinkingQuery = undefined;
718
+ activeThinkingStateQuery = undefined;
719
+ activeThinkingAnimationFrameQuery = undefined;
720
+ activeCompactSummarySetter = undefined;
721
+ compactSummaryActive = false;
677
722
  if (animationTimer) clearInterval(animationTimer);
678
723
  animationTimer = undefined;
679
724
  activeTui = undefined;
@@ -686,7 +731,11 @@ function compactThinking(pi: ExtensionAPI) {
686
731
  if (ctx.mode === "tui") ctx.ui.setWidget(WIDGET_ID, undefined);
687
732
 
688
733
  if (patchInstalled) {
689
- prototype.updateContent = originalUpdateContent;
734
+ if (prototype.updateContent === installedUpdateContent) {
735
+ const wrappedOriginal = (originalUpdateContent as any)[PROTOTYPE_ORIGINAL_KEY];
736
+ prototype.updateContent =
737
+ typeof wrappedOriginal === "function" ? wrappedOriginal : originalUpdateContent;
738
+ }
690
739
  patchInstalled = false;
691
740
  }
692
741
  });
@@ -795,5 +844,17 @@ export function installCompactThinking(
795
844
  Object.assign(initialConfig, next);
796
845
  Object.assign(config, next);
797
846
  },
847
+ getMessageThinkingDurationMs(messageTimestamp) {
848
+ return activeThinkingQuery?.(messageTimestamp);
849
+ },
850
+ isMessageThinkingActive(messageTimestamp) {
851
+ return activeThinkingStateQuery?.(messageTimestamp) ?? false;
852
+ },
853
+ getThinkingAnimationFrame() {
854
+ return activeThinkingAnimationFrameQuery?.() ?? 0;
855
+ },
856
+ setCompactSummaryActive(active) {
857
+ activeCompactSummarySetter?.(active);
858
+ },
798
859
  };
799
860
  }
@@ -639,7 +639,7 @@ export default function contextUsageExtension(pi: ExtensionAPI) {
639
639
  );
640
640
 
641
641
  if (!action) break;
642
- const preview = previewByKey.get(action);
642
+ const preview = previewByKey.get(action as PreviewKey);
643
643
  if (!preview) continue;
644
644
 
645
645
  await showTextPreview(ctx, preview.title, preview.content);
@@ -2,7 +2,7 @@ export const SESSION_REFERENCE_CUSTOM_TYPE = "session-reference";
2
2
  export const SESSION_REFERENCE_PREFIX = "@session:";
3
3
 
4
4
  const SESSION_REFERENCE_PATTERN =
5
- /(?:^|\s)@session:([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?)(?![A-Za-z0-9._-])/g;
5
+ /(?:^|\s)@session:(?:\[([^\]]+)\]|([A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?))(?![\w.\]-])/g;
6
6
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f]/g;
7
7
 
8
8
  export interface ReferenceSessionInfo {
@@ -31,12 +31,13 @@ export const DEFAULT_REFERENCE_LIMITS: ReferenceLimits = {
31
31
  maxTotalBytes: 48_000,
32
32
  };
33
33
 
34
+ /** 提取 @session:[name](组 1)或旧格式 @session:id(组 2),返回 name 或 id。 */
34
35
  export function extractSessionReferenceIds(text: string): string[] {
35
36
  const ids: string[] = [];
36
37
  const seen = new Set<string>();
37
38
 
38
39
  for (const match of text.matchAll(SESSION_REFERENCE_PATTERN)) {
39
- const id = match[1];
40
+ const id = (match[1] ?? match[2])?.trim();
40
41
  if (id && !seen.has(id)) {
41
42
  seen.add(id);
42
43
  ids.push(id);
@@ -154,39 +154,47 @@ function formatDate(date: Date): string {
154
154
 
155
155
  /** Stable across keystrokes while getReferences returns the same reference objects. */
156
156
  const sessionSearchTextCache = new WeakMap<SessionReference, string>();
157
- const sessionItemCache = new WeakMap<SessionReference, { cwd: string; item: AutocompleteItem }>();
157
+ const sessionItemCache = new WeakMap<
158
+ SessionReference,
159
+ { cwd: string; named?: AutocompleteItem; stable?: AutocompleteItem }
160
+ >();
158
161
 
159
162
  function sessionSearchText(reference: SessionReference): string {
160
163
  const cached = sessionSearchTextCache.get(reference);
161
164
  if (cached !== undefined) return cached;
162
- const text = [
163
- reference.info.name,
164
- reference.info.firstMessage,
165
- reference.info.cwd,
166
- reference.info.id,
167
- ...reference.referenceIds,
168
- ]
169
- .filter(Boolean)
170
- .join(" ");
165
+ // 会话只按显式 session name 匹配,避免 firstMessage、路径或 ID 产生噪声。
166
+ const text = reference.info.name?.trim() ?? "";
171
167
  sessionSearchTextCache.set(reference, text);
172
168
  return text;
173
169
  }
174
170
 
175
- function sessionItem(reference: SessionReference, currentCwd: string): AutocompleteItem {
176
- const cached = sessionItemCache.get(reference);
177
- if (cached && cached.cwd === currentCwd) return cached.item;
171
+ function sessionItem(
172
+ reference: SessionReference,
173
+ currentCwd: string,
174
+ useStableId: boolean,
175
+ ): AutocompleteItem {
176
+ let cached = sessionItemCache.get(reference);
177
+ const variant = useStableId ? "stable" : "named";
178
+ if (cached?.cwd === currentCwd && cached[variant]) return cached[variant];
178
179
 
179
180
  const session = reference.info;
180
181
  const workspace = samePath(session.cwd, currentCwd)
181
182
  ? "current workspace"
182
183
  : session.cwd || "unknown workspace";
183
184
  const label = reference.kind === "subagent" ? "[SubAgent]" : "[Session]";
185
+ // 唯一名称保持短格式;同名或无名称时使用稳定 ID,避免引用歧义。
186
+ const sessionName = session.name?.trim();
187
+ const referenceId = useStableId
188
+ ? reference.referenceIds[0]
189
+ : (sessionName ?? reference.referenceIds[0]);
184
190
  const item: AutocompleteItem = {
185
- value: `${SESSION_REFERENCE_PREFIX}${reference.referenceIds[0]}`,
191
+ value: `${SESSION_REFERENCE_PREFIX}[${referenceId}]`,
186
192
  label: `${label} ${sessionTitle(session)}`,
187
193
  description: `${workspace} · ${session.messageCount} messages · ${formatDate(session.modified)}`,
188
194
  };
189
- sessionItemCache.set(reference, { cwd: currentCwd, item });
195
+ if (cached?.cwd !== currentCwd) cached = { cwd: currentCwd };
196
+ cached[variant] = item;
197
+ sessionItemCache.set(reference, cached);
190
198
  return item;
191
199
  }
192
200
 
@@ -222,14 +230,24 @@ function filterSessions(
222
230
  if (query.startsWith("session:")) return [];
223
231
 
224
232
  const ordered = orderSessionReferences(references, currentCwd);
233
+ const nameCounts = new Map<string, number>();
234
+ for (const reference of ordered) {
235
+ const name = reference.info.name?.trim();
236
+ if (name) nameCounts.set(name, (nameCounts.get(name) ?? 0) + 1);
237
+ }
225
238
  const trimmed = query.trim();
226
- // Empty query: top-N already sorted — no fuzzy scan over the full list.
239
+ // 模糊匹配只接受有 session name 的会话;subagent 按 agent name 匹配。
240
+ const searchable = ordered.filter(
241
+ (reference) => reference.kind === "subagent" || Boolean(reference.info.name?.trim()),
242
+ );
243
+ // Empty query: top-N already sorted — no fuzzy scan over the full list。
227
244
  const matches = trimmed
228
- ? fuzzyFilter(ordered, trimmed, sessionSearchText)
245
+ ? fuzzyFilter(searchable, trimmed, sessionSearchText)
229
246
  : ordered.slice(0, MAX_SESSION_SUGGESTIONS);
230
- return matches
231
- .slice(0, MAX_SESSION_SUGGESTIONS)
232
- .map((reference) => sessionItem(reference, currentCwd));
247
+ return matches.slice(0, MAX_SESSION_SUGGESTIONS).map((reference) => {
248
+ const name = reference.info.name?.trim();
249
+ return sessionItem(reference, currentCwd, Boolean(name && (nameCounts.get(name) ?? 0) > 1));
250
+ });
233
251
  }
234
252
 
235
253
  function isPathLikeQuery(query: string): boolean {
@@ -470,12 +488,22 @@ export default function sessionReferenceExtension(pi: ExtensionAPI): void {
470
488
  ),
471
489
  ));
472
490
  const referencesById = new Map<string, SessionReference>();
491
+ const referencesByName = new Map<string, SessionReference>();
492
+ const ambiguousNames = new Set<string>();
473
493
  for (const reference of references) {
474
494
  for (const id of reference.referenceIds) referencesById.set(id, reference);
495
+ const name = reference.info.name?.trim();
496
+ if (!name || ambiguousNames.has(name)) continue;
497
+ if (referencesByName.has(name)) {
498
+ referencesByName.delete(name);
499
+ ambiguousNames.add(name);
500
+ } else {
501
+ referencesByName.set(name, reference);
502
+ }
475
503
  }
476
504
  const seenReferences = new Set<SessionReference>();
477
505
  const matchingReferences = referenceIds
478
- .map((id) => referencesById.get(id))
506
+ .map((id) => referencesById.get(id) ?? referencesByName.get(id))
479
507
  .filter((reference): reference is SessionReference => {
480
508
  if (!reference || reference.info.id === currentSessionId || seenReferences.has(reference))
481
509
  return false;
@@ -22,6 +22,18 @@ export default function (pi: ExtensionAPI): void {
22
22
  updateConfig(next) {
23
23
  compactThinking?.updateConfig(next);
24
24
  },
25
+ getMessageThinkingDurationMs(messageTimestamp) {
26
+ return compactThinking?.getMessageThinkingDurationMs?.(messageTimestamp);
27
+ },
28
+ isMessageThinkingActive(messageTimestamp) {
29
+ return compactThinking?.isMessageThinkingActive?.(messageTimestamp) ?? false;
30
+ },
31
+ getThinkingAnimationFrame() {
32
+ return compactThinking?.getThinkingAnimationFrame?.() ?? 0;
33
+ },
34
+ setCompactSummaryActive(active) {
35
+ compactThinking?.setCompactSummaryActive?.(active);
36
+ },
25
37
  };
26
38
  claudeCodeStyle(pi, undefined, compactThinkingBridge);
27
39
  compactThinking = installCompactThinking(pi, getCompactThinkingConfig());