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.
- package/README.en.md +9 -12
- package/README.md +9 -12
- package/extensions/config/config.ts +9 -4
- package/extensions/config/panel.ts +112 -20
- package/extensions/feature/agent-autocomplete.ts +3 -1
- package/extensions/feature/agent-summary.ts +2 -2
- package/extensions/feature/compact-thinking.ts +122 -65
- package/extensions/feature/context.ts +1 -1
- package/extensions/feature/markdown-enhance.ts +14 -0
- package/extensions/feature/session-reference-core.ts +3 -2
- package/extensions/feature/session-reference.ts +49 -21
- package/extensions/index.ts +12 -0
- package/extensions/renderer/compact-mode.ts +950 -0
- package/extensions/renderer/index.ts +73 -17
- package/extensions/renderer/message-display.ts +1 -1
- package/extensions/renderer/mouse-hover.ts +130 -0
- package/extensions/renderer/mouse-interaction.ts +74 -532
- package/extensions/renderer/mouse-layout.ts +160 -0
- package/extensions/renderer/mouse-packets.ts +143 -0
- package/extensions/renderer/mouse-scroll.ts +184 -0
- package/extensions/renderer/tool-diff/diff-renderer.ts +59 -2
- package/extensions/renderer/tool-grouping.ts +9 -2
- package/extensions/renderer/tool-result.ts +24 -8
- package/extensions/renderer/transcript-refresh.ts +91 -0
- package/extensions/utils/agent-summary.ts +1 -1
- package/package.json +1 -1
- package/themes/cc-dark.json +1 -0
- package/themes/cc-light.json +2 -2
|
@@ -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
|
|
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
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
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}` :
|
|
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 =
|
|
532
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
//
|
|
665
|
-
//
|
|
666
|
-
//
|
|
667
|
-
|
|
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
|
-
|
|
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
|
|
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])?)(?![
|
|
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<
|
|
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
|
-
|
|
163
|
-
|
|
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(
|
|
176
|
-
|
|
177
|
-
|
|
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}${
|
|
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
|
-
|
|
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
|
-
//
|
|
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(
|
|
245
|
+
? fuzzyFilter(searchable, trimmed, sessionSearchText)
|
|
229
246
|
: ordered.slice(0, MAX_SESSION_SUGGESTIONS);
|
|
230
|
-
return matches
|
|
231
|
-
.
|
|
232
|
-
|
|
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;
|
package/extensions/index.ts
CHANGED
|
@@ -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());
|