pi-cc-extensions 0.8.51 → 0.8.53

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,14 @@ 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
+ import { refreshMountedTranscript } from "../renderer/transcript-refresh.ts";
22
+ // 保持导出兼容:渲染函数已并入 renderer/compact-mode.ts,这里 re-export。
23
+ export { animateCompactThinkingText, formatThoughtDuration, styleCompactThinkingText };
16
24
 
17
25
  // pi-tui 类型声明中 TUI 的 re-export 解析失败,本地用最小结构化类型(只用到 requestRender)。
18
26
  type RenderTui = { requestRender(force?: boolean): void };
@@ -28,6 +36,18 @@ export type CompactThinkingConfig = {
28
36
 
29
37
  export type CompactThinkingController = {
30
38
  updateConfig(next: CompactThinkingConfig): void;
39
+ /**
40
+ * 只读查询:某条 assistant message 的可见思考总时长(ms)。
41
+ * 供 compact 渲染层生成 `Thought for 8s` 摘要,不建立第二套计时器。
42
+ * 无记录且无进行中思考时返回 undefined。
43
+ */
44
+ getMessageThinkingDurationMs?(messageTimestamp: number): number | undefined;
45
+ /** 当前消息是否仍处于 compact-thinking 的活动思考态。 */
46
+ isMessageThinkingActive?(messageTimestamp: number): boolean;
47
+ /** 复用 compact-thinking 配置的动画帧,不建立独立动画计时器。 */
48
+ getThinkingAnimationFrame?(): number;
49
+ /** compact 摘要保持 loading 时复用 compact-thinking 的动画循环。 */
50
+ setCompactSummaryActive?(active: boolean): void;
31
51
  };
32
52
 
33
53
  type DurationEntryData = {
@@ -86,6 +106,16 @@ const config: CompactThinkingConfig = {
86
106
  };
87
107
 
88
108
  const DURATION_ENTRY_TYPE = "compact-thinking-duration";
109
+ const COMPACT_THINKING_PATCH_KEY = Symbol.for("pi.ccstyle.compact-thinking-update");
110
+ const PROTOTYPE_ORIGINAL_KEY = Symbol.for("pi.ccstyle.prototype-original");
111
+
112
+ /** 当前激活 session 的只读查询委托(compact 渲染层使用)。 */
113
+ type ThinkingDurationQuery = (messageTimestamp: number) => number | undefined;
114
+ type ThinkingActiveQuery = (messageTimestamp: number) => boolean;
115
+ let activeThinkingQuery: ThinkingDurationQuery | undefined;
116
+ let activeThinkingStateQuery: ThinkingActiveQuery | undefined;
117
+ let activeThinkingAnimationFrameQuery: (() => number) | undefined;
118
+ let activeCompactSummarySetter: ((active: boolean) => void) | undefined;
89
119
 
90
120
  function restoreDurationEntries(
91
121
  entries: Array<{ type: string; customType?: string; data?: unknown }>,
@@ -124,18 +154,6 @@ function restoreDurationEntries(
124
154
  }
125
155
  }
126
156
 
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
157
  function getThinkingToggleHint() {
140
158
  const keys = getKeybindings().getKeys("app.thinking.toggle");
141
159
  return keys.length > 0 ? `${keys.join("/")} to expand` : undefined;
@@ -253,8 +271,22 @@ function compactThinking(pi: ExtensionAPI) {
253
271
  const prototype = AssistantMessageComponent.prototype as PatchedPrototype;
254
272
  const originalUpdateContent = prototype.updateContent;
255
273
 
274
+ activeThinkingStateQuery = (messageTimestamp) =>
275
+ activeThinking?.messageTimestamp === messageTimestamp;
276
+ activeThinkingAnimationFrameQuery = () => animationFrame;
277
+ activeThinkingQuery = (messageTimestamp) => {
278
+ let total = 0;
279
+ const durations = completedDurations.get(messageTimestamp);
280
+ if (durations) {
281
+ for (const duration of durations.values()) total += duration;
282
+ }
283
+ if (activeThinking?.messageTimestamp === messageTimestamp) {
284
+ total += Math.max(1, Date.now() - activeThinking.startedAt);
285
+ }
286
+ return total > 0 ? total : undefined;
287
+ };
288
+
256
289
  const completedDurations = new Map<number, Map<number, number>>();
257
- const renderedComponents = new Set<AssistantMessageComponentLike>();
258
290
  const streamingComponents = new Set<AssistantMessageComponentLike>();
259
291
  let activeThinking: ActiveThinking | undefined;
260
292
  let activeTheme: Theme | undefined;
@@ -263,46 +295,24 @@ 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
- function refreshRenderedComponents() {
269
- for (const component of renderedComponents) {
270
- const self = component as unknown as AssistantInternals;
271
- if (self.lastMessage) self.updateContent(self.lastMessage);
272
- }
273
- activeTui?.requestRender(true);
274
- }
275
-
276
301
  function thinkingStyle(text: string) {
277
- return activeTheme ? activeTheme.italic(activeTheme.fg("thinkingText", text)) : text;
302
+ return styleCompactThinkingText(text, activeTheme);
278
303
  }
279
304
 
280
305
  function summaryTitleStyle(text: string) {
281
- return activeTheme
282
- ? activeTheme.italic(activeTheme.bold(activeTheme.fg("thinkingText", text)))
283
- : text;
306
+ return styleCompactThinkingText(text, activeTheme, true);
284
307
  }
285
308
 
286
309
  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)
310
+ if (!animate) return baseStyle(text);
311
+ return animateCompactThinkingText(
312
+ text,
313
+ activeTheme,
314
+ animationFrame,
315
+ baseStyle === summaryTitleStyle,
306
316
  );
307
317
  }
308
318
 
@@ -328,7 +338,6 @@ function compactThinking(pi: ExtensionAPI) {
328
338
  const component = this as AssistantMessageComponentLike;
329
339
  const self = this as unknown as AssistantInternals;
330
340
  self.lastMessage = message;
331
- renderedComponents.add(component);
332
341
  latestComponent = component;
333
342
  latestComponentTimestamp = message.timestamp;
334
343
 
@@ -437,8 +446,10 @@ function compactThinking(pi: ExtensionAPI) {
437
446
  (durationText ? thinkingStyle(` · ${durationText}`) : "");
438
447
  } else {
439
448
  // Completed compact blocks use one provider-independent status line.
449
+ // 无时长记录(中断流/旧会话/entry 缺失或索引错位)时不回退到加载文案
450
+ // "Thinking...":改用摘要标题或中性 "Thought"。
440
451
  heading = thinkingStyle(
441
- durationText ? `Thought for ${durationText}` : self.hiddenThinkingLabel || "Thinking...",
452
+ durationText ? `Thought for ${durationText}` : (latestSummary?.title ?? "Thought"),
442
453
  );
443
454
  }
444
455
  self.contentContainer.addChild(new Text(heading, self.outputPad, 0));
@@ -506,6 +517,33 @@ function compactThinking(pi: ExtensionAPI) {
506
517
  }
507
518
  }
508
519
  };
520
+ const installedUpdateContent = prototype.updateContent;
521
+ (installedUpdateContent as any)[COMPACT_THINKING_PATCH_KEY] = true;
522
+
523
+ function ensureAnimationTimer() {
524
+ if (animationTimer) return;
525
+ animationTimer = setInterval(() => {
526
+ animationFrame++;
527
+ for (const component of streamingComponents) {
528
+ const self = component as unknown as AssistantInternals;
529
+ if (self.lastMessage) self.updateContent(self.lastMessage);
530
+ }
531
+ activeTui?.requestRender();
532
+ }, config.animationIntervalMs);
533
+ }
534
+
535
+ function stopAnimationTimerIfIdle() {
536
+ if (activeThinking || compactSummaryActive || !animationTimer) return;
537
+ clearInterval(animationTimer);
538
+ animationTimer = undefined;
539
+ }
540
+
541
+ activeCompactSummarySetter = (active) => {
542
+ compactSummaryActive = active;
543
+ if (active) ensureAnimationTimer();
544
+ else stopAnimationTimerIfIdle();
545
+ activeTui?.requestRender();
546
+ };
509
547
 
510
548
  function startThinking(message: AssistantMessage, contentIndex: number) {
511
549
  activeThinking = {
@@ -528,14 +566,8 @@ function compactThinking(pi: ExtensionAPI) {
528
566
  }
529
567
 
530
568
  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);
569
+ animationTimer = undefined;
570
+ ensureAnimationTimer();
539
571
  }
540
572
 
541
573
  function finishThinking() {
@@ -555,8 +587,7 @@ function compactThinking(pi: ExtensionAPI) {
555
587
  } as DurationEntryData);
556
588
 
557
589
  activeThinking = undefined;
558
- if (animationTimer) clearInterval(animationTimer);
559
- animationTimer = undefined;
590
+ stopAnimationTimerIfIdle();
560
591
 
561
592
  const components = [...streamingComponents];
562
593
  streamingComponents.clear();
@@ -589,7 +620,7 @@ function compactThinking(pi: ExtensionAPI) {
589
620
  content?.type === "toolCall" &&
590
621
  (content.name === "Agent" ||
591
622
  content.name === "Agents" ||
592
- content.args?.subagent_type != null),
623
+ content.arguments?.subagent_type != null),
593
624
  )
594
625
  );
595
626
  }
@@ -633,7 +664,9 @@ function compactThinking(pi: ExtensionAPI) {
633
664
  // OpenAI-compatible providers may not close reasoning until the response ends.
634
665
  pi.on("tool_execution_start", (event: any) => {
635
666
  if (event.toolName === "Agent" || event.toolName === "Agents") {
636
- resumeAgentThinking(latestComponent?.lastMessage ?? undefined);
667
+ const lastMessage = (latestComponent as unknown as { lastMessage?: AssistantMessage })
668
+ ?.lastMessage;
669
+ resumeAgentThinking(lastMessage);
637
670
  } else {
638
671
  finishThinking();
639
672
  }
@@ -656,24 +689,33 @@ function compactThinking(pi: ExtensionAPI) {
656
689
 
657
690
  // An empty widget gives the animation loop access to requestRender without
658
691
  // enabling terminal mouse reporting or intercepting native scrollback input.
692
+ // setWidget 的 factory 同步执行,此刻补丁已安装。
659
693
  ctx.ui.setWidget(WIDGET_ID, (tui) => {
660
694
  activeTui = tui;
661
695
  return { render: () => [], invalidate() {} };
662
696
  });
663
697
 
664
- // On resume, Pi may construct the chat before session_start is emitted.
665
- // Rebuild those already-rendered components now that persisted durations
666
- // and the active theme are available.
667
- refreshRenderedComponents();
698
+ // pi 在 reload/resume 时先于 session_start 用原始原型重建聊天组件
699
+ // (rebuildChatFromMessages / renderCurrentSessionState)。由共享的
700
+ // refreshMountedTranscript 扫描挂载树重绘这些组件,恢复 compact 渲染、
701
+ // 持久化时长与工具调用显示。
702
+ refreshMountedTranscript(activeTui);
703
+ activeTui?.requestRender(true);
668
704
  });
669
705
 
670
706
  pi.on("session_tree", (_event, ctx) => {
671
707
  restoreDurationEntries(ctx.sessionManager.getBranch(), completedDurations);
672
- refreshRenderedComponents();
708
+ refreshMountedTranscript(activeTui);
709
+ activeTui?.requestRender(true);
673
710
  });
674
711
 
675
712
  pi.on("session_shutdown", (_event, ctx) => {
676
713
  finishThinking();
714
+ activeThinkingQuery = undefined;
715
+ activeThinkingStateQuery = undefined;
716
+ activeThinkingAnimationFrameQuery = undefined;
717
+ activeCompactSummarySetter = undefined;
718
+ compactSummaryActive = false;
677
719
  if (animationTimer) clearInterval(animationTimer);
678
720
  animationTimer = undefined;
679
721
  activeTui = undefined;
@@ -681,12 +723,15 @@ function compactThinking(pi: ExtensionAPI) {
681
723
  latestComponent = undefined;
682
724
  latestComponentTimestamp = undefined;
683
725
  completedDurations.clear();
684
- renderedComponents.clear();
685
726
  streamingComponents.clear();
686
727
  if (ctx.mode === "tui") ctx.ui.setWidget(WIDGET_ID, undefined);
687
728
 
688
729
  if (patchInstalled) {
689
- prototype.updateContent = originalUpdateContent;
730
+ if (prototype.updateContent === installedUpdateContent) {
731
+ const wrappedOriginal = (originalUpdateContent as any)[PROTOTYPE_ORIGINAL_KEY];
732
+ prototype.updateContent =
733
+ typeof wrappedOriginal === "function" ? wrappedOriginal : originalUpdateContent;
734
+ }
690
735
  patchInstalled = false;
691
736
  }
692
737
  });
@@ -795,5 +840,17 @@ export function installCompactThinking(
795
840
  Object.assign(initialConfig, next);
796
841
  Object.assign(config, next);
797
842
  },
843
+ getMessageThinkingDurationMs(messageTimestamp) {
844
+ return activeThinkingQuery?.(messageTimestamp);
845
+ },
846
+ isMessageThinkingActive(messageTimestamp) {
847
+ return activeThinkingStateQuery?.(messageTimestamp) ?? false;
848
+ },
849
+ getThinkingAnimationFrame() {
850
+ return activeThinkingAnimationFrameQuery?.() ?? 0;
851
+ },
852
+ setCompactSummaryActive(active) {
853
+ activeCompactSummarySetter?.(active);
854
+ },
798
855
  };
799
856
  }
@@ -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);
@@ -129,6 +129,18 @@ function linkifyUrls(markdown: string): string {
129
129
  return out.join("\n");
130
130
  }
131
131
 
132
+ // ============================================================================
133
+ // 圈数字 → 半角括号
134
+ // ============================================================================
135
+
136
+ // Nerd Font 补丁字形(U+2460-U+2473)ink 超界,DirectWrite 下渲染会压住相邻字符;
137
+ // 转成 ASCII "(n)" 后任何字体下宽度一致。U+2460-U+2473 连续,差值即序号。
138
+ const CIRCLED_RE = /[①-⑳]/g;
139
+
140
+ function deCircled(markdown: string): string {
141
+ return markdown.replace(CIRCLED_RE, (ch) => `(${ch.codePointAt(0)! - 0x2460 + 1})`);
142
+ }
143
+
132
144
  // ============================================================================
133
145
  // 注册
134
146
  // ============================================================================
@@ -140,6 +152,8 @@ export default function (pi: ExtensionAPI): void {
140
152
  const { messageType, isStreaming = false } = context ?? {};
141
153
  // 与官方推荐一致:thinking 块与流式中间态不转换
142
154
  if (messageType === "assistant-thinking" || isStreaming) return markdown;
155
+ // 0. 圈数字转半角括号(Nerd Font 补丁字形缺陷规避)
156
+ markdown = deCircled(markdown);
143
157
  // 1. Mermaid 方言渲染
144
158
  markdown = renderDiagrams(markdown, context);
145
159
  // 2. GitHub 风格提示框
@@ -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());