pi-one-ui 0.1.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.
- package/LICENSE +21 -0
- package/README.md +106 -0
- package/extensions/app/config/renderer.ts +452 -0
- package/extensions/app/config/shell.ts +2484 -0
- package/extensions/app/ownership.ts +18 -0
- package/extensions/app/panel.ts +533 -0
- package/extensions/app/presets.ts +36 -0
- package/extensions/features/agent-summary/core.ts +194 -0
- package/extensions/features/agent-summary/index.ts +37 -0
- package/extensions/features/compact-thinking.ts +1262 -0
- package/extensions/features/context.ts +857 -0
- package/extensions/features/reference/index.ts +643 -0
- package/extensions/features/reference/session.ts +278 -0
- package/extensions/features/reference/subagent.ts +218 -0
- package/extensions/features/shell/aliases.ts +28 -0
- package/extensions/features/shell/flush-docked-bash.ts +39 -0
- package/extensions/features/shell/startup-header.ts +357 -0
- package/extensions/features/shell/working-message.ts +226 -0
- package/extensions/index.ts +52 -0
- package/extensions/shell/accent-rail-editor.ts +112 -0
- package/extensions/shell/accent-rail-layout-patch.ts +530 -0
- package/extensions/shell/completion-menu.ts +186 -0
- package/extensions/shell/editor-metadata-format.ts +287 -0
- package/extensions/shell/editor-transfer.ts +73 -0
- package/extensions/shell/extension-status.ts +102 -0
- package/extensions/shell/footer-format.ts +388 -0
- package/extensions/shell/footer-layout.ts +144 -0
- package/extensions/shell/footer.ts +1035 -0
- package/extensions/shell/format.ts +577 -0
- package/extensions/shell/git.ts +434 -0
- package/extensions/shell/icons.ts +261 -0
- package/extensions/shell/index.ts +1659 -0
- package/extensions/shell/interaction-summary.ts +1194 -0
- package/extensions/shell/live-context.ts +82 -0
- package/extensions/shell/minimalist-editor.ts +548 -0
- package/extensions/shell/package-version.ts +675 -0
- package/extensions/shell/project-refresh.ts +130 -0
- package/extensions/shell/project-state.ts +60 -0
- package/extensions/shell/prototype-patch-registry.ts +183 -0
- package/extensions/shell/runtime.ts +915 -0
- package/extensions/shell/selector-border.ts +109 -0
- package/extensions/shell/session-lifecycle.ts +60 -0
- package/extensions/shell/settings-command.ts +2144 -0
- package/extensions/shell/settings-previews.ts +137 -0
- package/extensions/shell/state.ts +83 -0
- package/extensions/shell/style.ts +480 -0
- package/extensions/shell/telemetry.ts +90 -0
- package/extensions/shell/ui.ts +1326 -0
- package/extensions/shell/user-message-osc.ts +279 -0
- package/extensions/shell/user-message-styles.ts +266 -0
- package/extensions/shell/user-message.ts +226 -0
- package/extensions/shell/working-line-messages.ts +24 -0
- package/extensions/shell/working-line-spinners.ts +54 -0
- package/extensions/shell/working-line.ts +1603 -0
- package/extensions/tools/ansi-color.ts +44 -0
- package/extensions/tools/ansi-text.ts +52 -0
- package/extensions/tools/component-tree.ts +37 -0
- package/extensions/tools/format.ts +28 -0
- package/extensions/tools/fullscreen-detect.ts +11 -0
- package/extensions/tools/patch-keys.ts +158 -0
- package/extensions/tools/sgr-mouse.ts +59 -0
- package/extensions/tools/tool-loading-icon.ts +20 -0
- package/extensions/tools/tool-result-sanitize.ts +27 -0
- package/extensions/transcript/index.ts +65 -0
- package/extensions/transcript/renderer/compact-mode.ts +1443 -0
- package/extensions/transcript/renderer/default-mode.ts +735 -0
- package/extensions/transcript/renderer/index.ts +286 -0
- package/extensions/transcript/renderer/markdown-enhance.ts +325 -0
- package/extensions/transcript/renderer/mouse/hover.ts +175 -0
- package/extensions/transcript/renderer/mouse/interaction.ts +1016 -0
- package/extensions/transcript/renderer/mouse/layout.ts +207 -0
- package/extensions/transcript/renderer/mouse/packets.ts +96 -0
- package/extensions/transcript/renderer/mouse/scroll.ts +211 -0
- package/extensions/transcript/renderer/tool/diff/ATTRIBUTION.md +28 -0
- package/extensions/transcript/renderer/tool/diff/ansi-utils.ts +128 -0
- package/extensions/transcript/renderer/tool/diff/diff-component.ts +112 -0
- package/extensions/transcript/renderer/tool/diff/diff-edit-render.ts +255 -0
- package/extensions/transcript/renderer/tool/diff/diff-header.ts +184 -0
- package/extensions/transcript/renderer/tool/diff/diff-highlight.ts +131 -0
- package/extensions/transcript/renderer/tool/diff/diff-inline.ts +350 -0
- package/extensions/transcript/renderer/tool/diff/diff-layout.ts +870 -0
- package/extensions/transcript/renderer/tool/diff/diff-limits.ts +189 -0
- package/extensions/transcript/renderer/tool/diff/diff-palette.ts +436 -0
- package/extensions/transcript/renderer/tool/diff/diff-parse.ts +536 -0
- package/extensions/transcript/renderer/tool/diff/diff-presentation.ts +79 -0
- package/extensions/transcript/renderer/tool/diff/diff-renderer.ts +29 -0
- package/extensions/transcript/renderer/tool/diff/diff-text.ts +57 -0
- package/extensions/transcript/renderer/tool/diff/diff-write-render.ts +629 -0
- package/extensions/transcript/renderer/tool/diff/index.ts +147 -0
- package/extensions/transcript/renderer/tool/diff/line-width-safety.ts +100 -0
- package/extensions/transcript/renderer/tool/diff/shiki-highlight.ts +104 -0
- package/extensions/transcript/renderer/tool/diff/write-execution.ts +124 -0
- package/extensions/transcript/renderer/tool/grouping.ts +619 -0
- package/extensions/transcript/renderer/tool/message-display.ts +183 -0
- package/extensions/transcript/renderer/tool/names.ts +195 -0
- package/extensions/transcript/renderer/tool/result.ts +842 -0
- package/extensions/transcript/renderer/tool/show-more-hint.ts +42 -0
- package/extensions/transcript/renderer/transcript-refresh.ts +79 -0
- package/package.json +58 -0
- package/themes/cc-dark.json +91 -0
- package/themes/cc-light.json +91 -0
|
@@ -0,0 +1,1262 @@
|
|
|
1
|
+
// compact-thinking 本地内联实现。fork 自 pi-compact-thinking(MIT,
|
|
2
|
+
// https://github.com/tifandotme/pi-extensions/tree/master/packages/pi-compact-thinking)。
|
|
3
|
+
// 差异:
|
|
4
|
+
// 1. 不再依赖上游包 —— 配置解耦,由 claude-code-style 经 installCompactThinking
|
|
5
|
+
// /updateConfig 管控(模块级 config 对象,不再读写 compact-thinking.json)。
|
|
6
|
+
// 2. 合并了上游 fork patch:subagent 工具(Agent/Agents)执行期间保持思考动画,
|
|
7
|
+
// 直到 tool_execution_end 或下一个 text/thinking 边界才收尾。
|
|
8
|
+
import type { AssistantMessage } from "@earendil-works/pi-ai";
|
|
9
|
+
import {
|
|
10
|
+
AssistantMessageComponent,
|
|
11
|
+
type ExtensionAPI,
|
|
12
|
+
type Theme,
|
|
13
|
+
} from "@earendil-works/pi-coding-agent";
|
|
14
|
+
import {
|
|
15
|
+
getKeybindings,
|
|
16
|
+
truncateToWidth,
|
|
17
|
+
visibleWidth,
|
|
18
|
+
wrapTextWithAnsi,
|
|
19
|
+
} from "@earendil-works/pi-tui";
|
|
20
|
+
import {
|
|
21
|
+
Box,
|
|
22
|
+
Markdown,
|
|
23
|
+
Spacer,
|
|
24
|
+
Text,
|
|
25
|
+
type Component,
|
|
26
|
+
} from "@earendil-works/pi-tui";
|
|
27
|
+
import { isToolTuiFullscreen } from "../transcript/renderer/tool/show-more-hint.ts";
|
|
28
|
+
import {
|
|
29
|
+
animateCompactThinkingText,
|
|
30
|
+
formatThoughtDuration,
|
|
31
|
+
styleCompactThinkingText,
|
|
32
|
+
} from "../transcript/renderer/compact-mode.ts";
|
|
33
|
+
import { refreshMountedTranscript } from "../transcript/renderer/transcript-refresh.ts";
|
|
34
|
+
import {
|
|
35
|
+
COMPACT_THINKING_OWNER,
|
|
36
|
+
COMPACT_THINKING_PATCH_KEY,
|
|
37
|
+
patchRegistry,
|
|
38
|
+
PROTOTYPE_ORIGINAL_KEY,
|
|
39
|
+
} from "../tools/patch-keys.ts";
|
|
40
|
+
// 保持导出兼容:渲染函数已并入 renderer/compact-mode.ts,这里 re-export。
|
|
41
|
+
export {
|
|
42
|
+
animateCompactThinkingText,
|
|
43
|
+
formatThoughtDuration,
|
|
44
|
+
styleCompactThinkingText,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// pi-tui 类型声明中 TUI 的 re-export 解析失败,本地用最小结构化类型(只用到 requestRender)。
|
|
48
|
+
type RenderTui = { requestRender(force?: boolean): void };
|
|
49
|
+
|
|
50
|
+
// pi-coding-agent 的类型声明中 AssistantMessageComponent 仅能以 value 形式使用。
|
|
51
|
+
type AssistantMessageComponentLike = InstanceType<
|
|
52
|
+
typeof AssistantMessageComponent
|
|
53
|
+
>;
|
|
54
|
+
|
|
55
|
+
export type CompactThinkingConfig = {
|
|
56
|
+
useSummaryTitlesAsThinkingTitle: boolean;
|
|
57
|
+
previewLines: number;
|
|
58
|
+
animationIntervalMs: number;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type CompactThinkingController = {
|
|
62
|
+
updateConfig(next: CompactThinkingConfig): void;
|
|
63
|
+
/**
|
|
64
|
+
* 只读查询:某条 assistant message 的可见思考总时长(ms)。
|
|
65
|
+
* 供 compact 渲染层生成 `Thought for 8s` 摘要,不建立第二套计时器。
|
|
66
|
+
* 无记录且无进行中思考时返回 undefined。
|
|
67
|
+
*/
|
|
68
|
+
getMessageThinkingDurationMs?(messageTimestamp: number): number | undefined;
|
|
69
|
+
/** 当前消息是否仍处于 compact-thinking 的活动思考态。 */
|
|
70
|
+
isMessageThinkingActive?(messageTimestamp: number): boolean;
|
|
71
|
+
/** 复用 compact-thinking 配置的动画帧,不建立独立动画计时器。 */
|
|
72
|
+
getThinkingAnimationFrame?(): number;
|
|
73
|
+
/** compact 摘要保持 loading 时复用 compact-thinking 的动画循环。 */
|
|
74
|
+
setCompactSummaryActive?(active: boolean): void;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type DurationEntryData = {
|
|
78
|
+
messageTimestamp: number;
|
|
79
|
+
contentIndex: number;
|
|
80
|
+
durationMs: number;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type SummaryPart = {
|
|
84
|
+
title: string;
|
|
85
|
+
body: string;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
type ActiveThinking = {
|
|
89
|
+
messageTimestamp: number;
|
|
90
|
+
contentIndex: number;
|
|
91
|
+
startedAt: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// pi-coding-agent 类型声明中 MarkdownTransformer 的 re-export 解析失败,本地用最小结构化类型。
|
|
95
|
+
type MarkdownTransformer = (
|
|
96
|
+
markdown: string,
|
|
97
|
+
context: {
|
|
98
|
+
messageType: "user" | "assistant" | "assistant-thinking";
|
|
99
|
+
isStreaming: boolean;
|
|
100
|
+
availableWidth: number;
|
|
101
|
+
},
|
|
102
|
+
) => string;
|
|
103
|
+
|
|
104
|
+
type AssistantInternals = {
|
|
105
|
+
contentContainer: {
|
|
106
|
+
clear(): void;
|
|
107
|
+
addChild(component: Component): void;
|
|
108
|
+
};
|
|
109
|
+
hideThinkingBlock: boolean;
|
|
110
|
+
markdownTheme: ConstructorParameters<typeof Markdown>[3];
|
|
111
|
+
hiddenThinkingLabel: string;
|
|
112
|
+
outputPad: number;
|
|
113
|
+
isStreaming: boolean;
|
|
114
|
+
markdownTransformers: readonly MarkdownTransformer[];
|
|
115
|
+
lastMessage?: AssistantMessage;
|
|
116
|
+
hasToolCalls: boolean;
|
|
117
|
+
updateContent(message: AssistantMessage): void;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
type PatchedPrototype = typeof AssistantMessageComponent.prototype & {
|
|
121
|
+
updateContent: (message: AssistantMessage, isStreaming?: boolean) => void;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// 配置由 claude-code-style 管控(installCompactThinking 注入初始值,
|
|
125
|
+
// updateConfig 运行时更新),上游的配置文件读写已移除。
|
|
126
|
+
const config: CompactThinkingConfig = {
|
|
127
|
+
useSummaryTitlesAsThinkingTitle: true,
|
|
128
|
+
previewLines: 3,
|
|
129
|
+
animationIntervalMs: 90,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const DURATION_ENTRY_TYPE = "compact-thinking-duration";
|
|
133
|
+
|
|
134
|
+
/** 当前激活 session 的只读查询委托(compact 渲染层使用)。 */
|
|
135
|
+
type ThinkingDurationQuery = (messageTimestamp: number) => number | undefined;
|
|
136
|
+
type ThinkingActiveQuery = (messageTimestamp: number) => boolean;
|
|
137
|
+
let activeThinkingQuery: ThinkingDurationQuery | undefined;
|
|
138
|
+
let activeThinkingStateQuery: ThinkingActiveQuery | undefined;
|
|
139
|
+
let activeThinkingAnimationFrameQuery: (() => number) | undefined;
|
|
140
|
+
let activeCompactSummarySetter: ((active: boolean) => void) | undefined;
|
|
141
|
+
|
|
142
|
+
function restoreDurationEntries(
|
|
143
|
+
entries: Array<{ type: string; customType?: string; data?: unknown }>,
|
|
144
|
+
completedDurations: Map<number, Map<number, number>>,
|
|
145
|
+
) {
|
|
146
|
+
completedDurations.clear();
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
if (
|
|
149
|
+
entry.type !== "custom" ||
|
|
150
|
+
entry.customType !== DURATION_ENTRY_TYPE ||
|
|
151
|
+
!entry.data ||
|
|
152
|
+
typeof entry.data !== "object"
|
|
153
|
+
) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const data = entry.data as Partial<DurationEntryData>;
|
|
158
|
+
if (
|
|
159
|
+
typeof data.messageTimestamp !== "number" ||
|
|
160
|
+
!Number.isFinite(data.messageTimestamp) ||
|
|
161
|
+
typeof data.contentIndex !== "number" ||
|
|
162
|
+
!Number.isInteger(data.contentIndex) ||
|
|
163
|
+
typeof data.durationMs !== "number" ||
|
|
164
|
+
!Number.isFinite(data.durationMs) ||
|
|
165
|
+
data.durationMs < 1
|
|
166
|
+
) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
let durations = completedDurations.get(data.messageTimestamp);
|
|
171
|
+
if (!durations) {
|
|
172
|
+
durations = new Map();
|
|
173
|
+
completedDurations.set(data.messageTimestamp, durations);
|
|
174
|
+
}
|
|
175
|
+
durations.set(data.contentIndex, data.durationMs);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function getThinkingToggleHint() {
|
|
180
|
+
const keys = getKeybindings().getKeys("app.thinking.toggle");
|
|
181
|
+
return keys.length > 0 ? `${keys.join("/")} to expand` : undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function thinkingExpandAction(): string | undefined {
|
|
185
|
+
return isToolTuiFullscreen() ? "click to show more" : getThinkingToggleHint();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 只 wrap 尾部窗口;缓存未着色折行,着色放到取出时做,避免主题切换命中旧 ANSI。
|
|
189
|
+
type PreviewCacheEntry = { lines: string[]; hiddenBefore: number };
|
|
190
|
+
const previewCache = new Map<string, PreviewCacheEntry>();
|
|
191
|
+
export const THINKING_PREVIEW_CACHE_MAX = 2_048;
|
|
192
|
+
const PREVIEW_BUDGET_MIN_CHARS = 2_000;
|
|
193
|
+
const PREVIEW_BUDGET_SLACK_LINES = 2;
|
|
194
|
+
|
|
195
|
+
function getCachedPreview(key: string): PreviewCacheEntry | undefined {
|
|
196
|
+
const entry = previewCache.get(key);
|
|
197
|
+
if (!entry) return undefined;
|
|
198
|
+
previewCache.delete(key);
|
|
199
|
+
previewCache.set(key, entry);
|
|
200
|
+
return entry;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function cachePreview(key: string, entry: PreviewCacheEntry): void {
|
|
204
|
+
previewCache.delete(key);
|
|
205
|
+
while (previewCache.size >= THINKING_PREVIEW_CACHE_MAX) {
|
|
206
|
+
const oldest = previewCache.keys().next().value;
|
|
207
|
+
if (oldest === undefined) break;
|
|
208
|
+
previewCache.delete(oldest);
|
|
209
|
+
}
|
|
210
|
+
previewCache.set(key, entry);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export function clearThinkingPreviewCache(): void {
|
|
214
|
+
previewCache.clear();
|
|
215
|
+
expandedWrapCache.clear();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type ExpandedWrapEntry = { text: string; lines: string[] };
|
|
219
|
+
const expandedWrapCache = new Map<string, ExpandedWrapEntry>();
|
|
220
|
+
export const THINKING_EXPANDED_WRAP_CACHE_MAX = 64;
|
|
221
|
+
|
|
222
|
+
function expandedWrapKey(
|
|
223
|
+
messageTimestamp: number,
|
|
224
|
+
runStartIndex: number,
|
|
225
|
+
width: number,
|
|
226
|
+
): string {
|
|
227
|
+
return `${messageTimestamp}:${runStartIndex}:${width}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function wrapExpandedThinking(
|
|
231
|
+
messageTimestamp: number,
|
|
232
|
+
runStartIndex: number,
|
|
233
|
+
text: string,
|
|
234
|
+
width: number,
|
|
235
|
+
): string[] {
|
|
236
|
+
const key = expandedWrapKey(messageTimestamp, runStartIndex, width);
|
|
237
|
+
const hit = expandedWrapCache.get(key);
|
|
238
|
+
if (hit && hit.text === text) {
|
|
239
|
+
expandedWrapCache.delete(key);
|
|
240
|
+
expandedWrapCache.set(key, hit);
|
|
241
|
+
return hit.lines;
|
|
242
|
+
}
|
|
243
|
+
const lines = wrapTextWithAnsi(
|
|
244
|
+
text.replace(/\t/g, " "),
|
|
245
|
+
Math.max(1, width),
|
|
246
|
+
);
|
|
247
|
+
expandedWrapCache.delete(key);
|
|
248
|
+
while (expandedWrapCache.size >= THINKING_EXPANDED_WRAP_CACHE_MAX) {
|
|
249
|
+
const oldest = expandedWrapCache.keys().next().value;
|
|
250
|
+
if (oldest === undefined) break;
|
|
251
|
+
expandedWrapCache.delete(oldest);
|
|
252
|
+
}
|
|
253
|
+
expandedWrapCache.set(key, { text, lines });
|
|
254
|
+
return lines;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function evictExpandedWraps(
|
|
258
|
+
messageTimestamp: number,
|
|
259
|
+
runStartIndex: number,
|
|
260
|
+
): void {
|
|
261
|
+
for (const key of expandedWrapCache.keys()) {
|
|
262
|
+
const [ts, run] = key.split(":");
|
|
263
|
+
if (Number(ts) === messageTimestamp && Number(run) === runStartIndex) {
|
|
264
|
+
expandedWrapCache.delete(key);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function thinkingPreviewCacheSize(): number {
|
|
270
|
+
return previewCache.size;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export function thinkingExpandedWrapCacheSize(): number {
|
|
274
|
+
return expandedWrapCache.size;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** 按可见宽度估算折行数,不走 Text 全量 wrap。无换行长段也能计到隐藏行。 */
|
|
278
|
+
function countWrappedLines(text: string, contentWidth: number): number {
|
|
279
|
+
if (!text) return 0;
|
|
280
|
+
const width = Math.max(1, contentWidth);
|
|
281
|
+
let lines = 0;
|
|
282
|
+
for (const raw of text.replace(/\t/g, " ").split(/\r\n|\r|\n/)) {
|
|
283
|
+
const w = visibleWidth(raw);
|
|
284
|
+
lines += w === 0 ? 1 : Math.ceil(w / width);
|
|
285
|
+
}
|
|
286
|
+
return lines;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function padPreviewLine(line: string, width: number, padding: number): string {
|
|
290
|
+
const left = padding > 0 ? " ".repeat(padding) : "";
|
|
291
|
+
const right = padding > 0 ? " ".repeat(padding) : "";
|
|
292
|
+
const withMargins = left + line + right;
|
|
293
|
+
return (
|
|
294
|
+
withMargins + " ".repeat(Math.max(0, width - visibleWidth(withMargins)))
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function layoutThinkingPreview(
|
|
299
|
+
text: string,
|
|
300
|
+
width: number,
|
|
301
|
+
padding: number,
|
|
302
|
+
): { visible: string[]; hiddenLines: number } {
|
|
303
|
+
if (!text || config.previewLines <= 0) return { visible: [], hiddenLines: 0 };
|
|
304
|
+
const contentWidth = Math.max(1, width - padding * 2);
|
|
305
|
+
const budget = Math.max(
|
|
306
|
+
width * (config.previewLines + PREVIEW_BUDGET_SLACK_LINES),
|
|
307
|
+
PREVIEW_BUDGET_MIN_CHARS,
|
|
308
|
+
);
|
|
309
|
+
let source = text;
|
|
310
|
+
let cut = 0;
|
|
311
|
+
if (source.length > budget) {
|
|
312
|
+
const start = source.length - budget;
|
|
313
|
+
const newline = source.indexOf("\n", start);
|
|
314
|
+
cut = newline === -1 ? start : newline + 1;
|
|
315
|
+
if (cut < source.length) source = source.slice(cut);
|
|
316
|
+
else cut = 0;
|
|
317
|
+
}
|
|
318
|
+
const cacheKey = `${width}:${padding}:${cut}:${source}`;
|
|
319
|
+
let entry = getCachedPreview(cacheKey);
|
|
320
|
+
if (!entry) {
|
|
321
|
+
entry = {
|
|
322
|
+
lines: wrapTextWithAnsi(source.replace(/\t/g, " "), contentWidth),
|
|
323
|
+
hiddenBefore:
|
|
324
|
+
cut > 0 ? countWrappedLines(text.slice(0, cut), contentWidth) : 0,
|
|
325
|
+
};
|
|
326
|
+
cachePreview(cacheKey, entry);
|
|
327
|
+
}
|
|
328
|
+
const hiddenLines =
|
|
329
|
+
entry.hiddenBefore + Math.max(0, entry.lines.length - config.previewLines);
|
|
330
|
+
const visible =
|
|
331
|
+
hiddenLines > 0 ? entry.lines.slice(-config.previewLines) : entry.lines;
|
|
332
|
+
return { visible, hiddenLines };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function hiddenPreviewHint(
|
|
336
|
+
hiddenLines: number,
|
|
337
|
+
forceExpandHint = false,
|
|
338
|
+
): { prefix: string; action: string; suffix: string } | undefined {
|
|
339
|
+
const action = thinkingExpandAction() ?? "";
|
|
340
|
+
if (hiddenLines > 0) {
|
|
341
|
+
const noun = hiddenLines === 1 ? "line" : "lines";
|
|
342
|
+
return {
|
|
343
|
+
prefix: ` • (${hiddenLines} more ${noun}${action ? ", " : ""}`,
|
|
344
|
+
action,
|
|
345
|
+
suffix: ")",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (forceExpandHint && isToolTuiFullscreen()) {
|
|
349
|
+
return { prefix: " • ", action: "click to show more", suffix: "" };
|
|
350
|
+
}
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const expandedThinking = new Set<number>();
|
|
355
|
+
|
|
356
|
+
/** 折叠预览 + 展开全文。fullscreen 点击 hint 展开、双击整块收起,对齐工具卡。 */
|
|
357
|
+
export class ThinkingPreviewBlock implements Component {
|
|
358
|
+
private heading: string;
|
|
359
|
+
private text: string;
|
|
360
|
+
private padding: number;
|
|
361
|
+
readonly messageTimestamp: number;
|
|
362
|
+
readonly runStartIndex: number;
|
|
363
|
+
private style: (text: string) => string;
|
|
364
|
+
private theme: Theme | undefined;
|
|
365
|
+
private _expanded: boolean;
|
|
366
|
+
private hintHovered = false;
|
|
367
|
+
private collapsedMemo:
|
|
368
|
+
| {
|
|
369
|
+
width: number;
|
|
370
|
+
previewLines: number;
|
|
371
|
+
hintHovered: boolean;
|
|
372
|
+
expandAction: string | undefined;
|
|
373
|
+
lines: string[];
|
|
374
|
+
}
|
|
375
|
+
| undefined;
|
|
376
|
+
|
|
377
|
+
constructor(
|
|
378
|
+
heading: string,
|
|
379
|
+
text: string,
|
|
380
|
+
padding: number,
|
|
381
|
+
messageTimestamp: number,
|
|
382
|
+
style: (text: string) => string,
|
|
383
|
+
theme?: Theme,
|
|
384
|
+
runStartIndex = 0,
|
|
385
|
+
) {
|
|
386
|
+
this.heading = heading;
|
|
387
|
+
this.text = text;
|
|
388
|
+
this.padding = padding;
|
|
389
|
+
this.messageTimestamp = messageTimestamp;
|
|
390
|
+
this.runStartIndex = runStartIndex;
|
|
391
|
+
this.style = style;
|
|
392
|
+
this.theme = theme;
|
|
393
|
+
this._expanded = expandedThinking.has(messageTimestamp);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private paint(color: string, text: string): string {
|
|
397
|
+
return this.theme && typeof this.theme.fg === "function"
|
|
398
|
+
? this.theme.fg(color as never, text)
|
|
399
|
+
: text;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
get expanded(): boolean {
|
|
403
|
+
return this._expanded;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
setExpanded(expanded: boolean): void {
|
|
407
|
+
if (this._expanded !== expanded) this.collapsedMemo = undefined;
|
|
408
|
+
this._expanded = expanded;
|
|
409
|
+
if (expanded) expandedThinking.add(this.messageTimestamp);
|
|
410
|
+
else {
|
|
411
|
+
expandedThinking.delete(this.messageTimestamp);
|
|
412
|
+
evictExpandedWraps(this.messageTimestamp, this.runStartIndex);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
setHintHovered(hovered: boolean): void {
|
|
417
|
+
if (this.hintHovered !== hovered) this.collapsedMemo = undefined;
|
|
418
|
+
this.hintHovered = hovered;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
private headingLines(
|
|
422
|
+
width: number,
|
|
423
|
+
hiddenLines: number,
|
|
424
|
+
padding: number,
|
|
425
|
+
): string[] {
|
|
426
|
+
const hint = this._expanded
|
|
427
|
+
? undefined
|
|
428
|
+
: hiddenPreviewHint(
|
|
429
|
+
hiddenLines,
|
|
430
|
+
Boolean(this.text) && config.previewLines <= 0,
|
|
431
|
+
);
|
|
432
|
+
if (!hint) return new Text(this.heading, padding, 0).render(width);
|
|
433
|
+
const rawHint = hint.prefix + hint.action + hint.suffix;
|
|
434
|
+
const contentWidth = Math.max(1, width - padding * 2);
|
|
435
|
+
const headingBudget = Math.max(0, contentWidth - visibleWidth(rawHint));
|
|
436
|
+
const clipped =
|
|
437
|
+
visibleWidth(this.heading) > headingBudget
|
|
438
|
+
? truncateToWidth(this.heading, headingBudget, "")
|
|
439
|
+
: this.heading;
|
|
440
|
+
const action = this.paint(this.hintHovered ? "text" : "dim", hint.action);
|
|
441
|
+
return new Text(
|
|
442
|
+
clipped +
|
|
443
|
+
this.paint("dim", hint.prefix) +
|
|
444
|
+
action +
|
|
445
|
+
this.paint("dim", hint.suffix),
|
|
446
|
+
padding,
|
|
447
|
+
0,
|
|
448
|
+
).render(width);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
private bodyLines(
|
|
452
|
+
width: number,
|
|
453
|
+
padding: number,
|
|
454
|
+
): { lines: string[]; hiddenLines: number } {
|
|
455
|
+
if (!this.text || (config.previewLines <= 0 && !this._expanded)) {
|
|
456
|
+
return { lines: [], hiddenLines: 0 };
|
|
457
|
+
}
|
|
458
|
+
if (this._expanded) {
|
|
459
|
+
return {
|
|
460
|
+
lines: wrapExpandedThinking(
|
|
461
|
+
this.messageTimestamp,
|
|
462
|
+
this.runStartIndex,
|
|
463
|
+
this.text,
|
|
464
|
+
width,
|
|
465
|
+
),
|
|
466
|
+
hiddenLines: 0,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
const preview = layoutThinkingPreview(this.text, width, padding);
|
|
470
|
+
return { lines: preview.visible, hiddenLines: preview.hiddenLines };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
render(width: number) {
|
|
474
|
+
if (this._expanded) {
|
|
475
|
+
const innerWidth = Math.max(1, width - 2);
|
|
476
|
+
const body = this.bodyLines(innerWidth, 0);
|
|
477
|
+
const heading = this.headingLines(innerWidth, 0, 0);
|
|
478
|
+
const inner = body.lines.length
|
|
479
|
+
? [...heading, ...body.lines.map((line) => this.style(line))]
|
|
480
|
+
: heading;
|
|
481
|
+
const bgFn =
|
|
482
|
+
this.theme && typeof this.theme.bg === "function"
|
|
483
|
+
? (text: string) => this.theme!.bg("userMessageBg" as never, text)
|
|
484
|
+
: undefined;
|
|
485
|
+
const box = new Box(1, 1, bgFn);
|
|
486
|
+
box.addChild({
|
|
487
|
+
render: () => inner,
|
|
488
|
+
invalidate() {},
|
|
489
|
+
});
|
|
490
|
+
return box.render(width);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const expandAction = thinkingExpandAction();
|
|
494
|
+
if (
|
|
495
|
+
this.collapsedMemo?.width === width &&
|
|
496
|
+
this.collapsedMemo.previewLines === config.previewLines &&
|
|
497
|
+
this.collapsedMemo.hintHovered === this.hintHovered &&
|
|
498
|
+
this.collapsedMemo.expandAction === expandAction
|
|
499
|
+
) {
|
|
500
|
+
return this.collapsedMemo.lines;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const body = this.bodyLines(width, this.padding);
|
|
504
|
+
const heading = this.headingLines(width, body.hiddenLines, this.padding);
|
|
505
|
+
const lines = body.lines.length
|
|
506
|
+
? [
|
|
507
|
+
...heading,
|
|
508
|
+
...body.lines.map((line) =>
|
|
509
|
+
padPreviewLine(this.style(line), width, this.padding),
|
|
510
|
+
),
|
|
511
|
+
]
|
|
512
|
+
: heading;
|
|
513
|
+
this.collapsedMemo = {
|
|
514
|
+
width,
|
|
515
|
+
previewLines: config.previewLines,
|
|
516
|
+
hintHovered: this.hintHovered,
|
|
517
|
+
expandAction,
|
|
518
|
+
lines,
|
|
519
|
+
};
|
|
520
|
+
return lines;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
invalidate() {
|
|
524
|
+
this.collapsedMemo = undefined;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function parseSummaryPart(text: string): SummaryPart | undefined {
|
|
529
|
+
const match =
|
|
530
|
+
/^\s*\*\*([^\n]+?)\*\*[ \t]*(?:\r?\n(?:\r?\n)?([\s\S]*))?\s*$/.exec(text);
|
|
531
|
+
if (!match) return undefined;
|
|
532
|
+
return { title: match[1].trim(), body: (match[2] ?? "").trim() };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function parseLatestStreamingSummary(text: string): SummaryPart | undefined {
|
|
536
|
+
// Providers do not consistently insert a blank line between streamed
|
|
537
|
+
// summary parts, so accept a bold title at the start of any source line.
|
|
538
|
+
const titlePattern = /(?:^|\r?\n)\s*\*\*([^\n*]+?)\*\*[ \t]*(?:\r?\n)?/g;
|
|
539
|
+
let latest: RegExpExecArray | undefined;
|
|
540
|
+
let match: RegExpExecArray | null;
|
|
541
|
+
while ((match = titlePattern.exec(text))) latest = match;
|
|
542
|
+
if (!latest) return parseSummaryPart(text);
|
|
543
|
+
|
|
544
|
+
return {
|
|
545
|
+
title: latest[1].trim(),
|
|
546
|
+
body: text.slice(latest.index + latest[0].length).trim(),
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function isOpenAiResponsesMessage(message: AssistantMessage) {
|
|
551
|
+
return (
|
|
552
|
+
message.api === "openai-responses" ||
|
|
553
|
+
message.api === "openai-codex-responses" ||
|
|
554
|
+
message.api === "azure-openai-responses"
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function getLatestOpenAiSummary(
|
|
559
|
+
thinkingSignature: string | undefined,
|
|
560
|
+
): SummaryPart | undefined {
|
|
561
|
+
if (!thinkingSignature) return undefined;
|
|
562
|
+
|
|
563
|
+
try {
|
|
564
|
+
const item = JSON.parse(thinkingSignature) as {
|
|
565
|
+
type?: unknown;
|
|
566
|
+
summary?: Array<{ type?: unknown; text?: unknown }>;
|
|
567
|
+
};
|
|
568
|
+
if (item.type !== "reasoning" || !Array.isArray(item.summary)) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
for (let i = item.summary.length - 1; i >= 0; i--) {
|
|
573
|
+
const part = item.summary[i];
|
|
574
|
+
if (part.type !== "summary_text" || typeof part.text !== "string") {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
const parsed = parseSummaryPart(part.text);
|
|
578
|
+
if (parsed) return parsed;
|
|
579
|
+
}
|
|
580
|
+
} catch {
|
|
581
|
+
// Invalid or provider-specific signatures use the generic fallback.
|
|
582
|
+
}
|
|
583
|
+
return undefined;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const WIDGET_ID = "compact-thinking-render-loop";
|
|
587
|
+
|
|
588
|
+
// 上游 index.ts 内联(含 subagent fork patch)。
|
|
589
|
+
function createTransform(
|
|
590
|
+
messageType: "assistant" | "assistant-thinking",
|
|
591
|
+
self: AssistantInternals,
|
|
592
|
+
) {
|
|
593
|
+
// 与 pi 内置 applyMarkdownTransformers 相同的链式语义:
|
|
594
|
+
// 异常保留当前 markdown,继续下一个 transformer。
|
|
595
|
+
return (markdown: string, availableWidth: number): string => {
|
|
596
|
+
let out = markdown;
|
|
597
|
+
for (const transformer of self.markdownTransformers ?? []) {
|
|
598
|
+
try {
|
|
599
|
+
const result = transformer(out, {
|
|
600
|
+
messageType,
|
|
601
|
+
isStreaming: self.isStreaming,
|
|
602
|
+
availableWidth,
|
|
603
|
+
});
|
|
604
|
+
if (typeof result === "string") out = result;
|
|
605
|
+
} catch {
|
|
606
|
+
// 保持当前 markdown 继续
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return out;
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function compactThinking(pi: ExtensionAPI) {
|
|
614
|
+
const prototype = AssistantMessageComponent.prototype as PatchedPrototype;
|
|
615
|
+
const originalUpdateContent = prototype.updateContent;
|
|
616
|
+
|
|
617
|
+
activeThinkingStateQuery = (messageTimestamp) =>
|
|
618
|
+
activeThinking?.messageTimestamp === messageTimestamp;
|
|
619
|
+
activeThinkingAnimationFrameQuery = () => animationFrame;
|
|
620
|
+
activeThinkingQuery = (messageTimestamp) => {
|
|
621
|
+
let total = 0;
|
|
622
|
+
const durations = completedDurations.get(messageTimestamp);
|
|
623
|
+
if (durations) {
|
|
624
|
+
for (const duration of durations.values()) total += duration;
|
|
625
|
+
}
|
|
626
|
+
if (activeThinking?.messageTimestamp === messageTimestamp) {
|
|
627
|
+
total += Math.max(1, Date.now() - activeThinking.startedAt);
|
|
628
|
+
}
|
|
629
|
+
return total > 0 ? total : undefined;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const completedDurations = new Map<number, Map<number, number>>();
|
|
633
|
+
const streamingComponents = new Set<AssistantMessageComponentLike>();
|
|
634
|
+
let activeThinking: ActiveThinking | undefined;
|
|
635
|
+
let activeTheme: Theme | undefined;
|
|
636
|
+
let activeTui: RenderTui | undefined;
|
|
637
|
+
let latestComponent: AssistantMessageComponentLike | undefined;
|
|
638
|
+
let latestComponentTimestamp: number | undefined;
|
|
639
|
+
let animationTimer: ReturnType<typeof setInterval> | undefined;
|
|
640
|
+
let animationFrame = 0;
|
|
641
|
+
let compactSummaryActive = false;
|
|
642
|
+
let patchInstalled = true;
|
|
643
|
+
|
|
644
|
+
function thinkingStyle(text: string) {
|
|
645
|
+
return styleCompactThinkingText(text, activeTheme);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function summaryTitleStyle(text: string) {
|
|
649
|
+
return styleCompactThinkingText(text, activeTheme, true);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function animatedText(
|
|
653
|
+
text: string,
|
|
654
|
+
baseStyle: (value: string) => string,
|
|
655
|
+
animate: boolean,
|
|
656
|
+
) {
|
|
657
|
+
if (!animate) return baseStyle(text);
|
|
658
|
+
return animateCompactThinkingText(
|
|
659
|
+
text,
|
|
660
|
+
activeTheme,
|
|
661
|
+
animationFrame,
|
|
662
|
+
baseStyle === summaryTitleStyle,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function getCompletedDuration(
|
|
667
|
+
messageTimestamp: number,
|
|
668
|
+
startIndex: number,
|
|
669
|
+
endIndex: number,
|
|
670
|
+
) {
|
|
671
|
+
const durations = completedDurations.get(messageTimestamp);
|
|
672
|
+
if (!durations) return undefined;
|
|
673
|
+
for (let index = endIndex; index >= startIndex; index--) {
|
|
674
|
+
const duration = durations.get(index);
|
|
675
|
+
if (duration !== undefined) return duration;
|
|
676
|
+
}
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function isActiveRun(
|
|
681
|
+
message: AssistantMessage,
|
|
682
|
+
startIndex: number,
|
|
683
|
+
endIndex: number,
|
|
684
|
+
) {
|
|
685
|
+
return (
|
|
686
|
+
activeThinking?.messageTimestamp === message.timestamp &&
|
|
687
|
+
activeThinking.contentIndex >= startIndex &&
|
|
688
|
+
activeThinking.contentIndex <= endIndex
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
prototype.updateContent = function patchedUpdateContent(
|
|
693
|
+
message: AssistantMessage,
|
|
694
|
+
isStreaming?: boolean,
|
|
695
|
+
) {
|
|
696
|
+
const component = this as AssistantMessageComponentLike;
|
|
697
|
+
const self = this as unknown as AssistantInternals;
|
|
698
|
+
// isStreaming 丢失 → mermaid 流式误渲染来回闪。
|
|
699
|
+
self.isStreaming = isStreaming ?? self.isStreaming;
|
|
700
|
+
self.lastMessage = message;
|
|
701
|
+
latestComponent = component;
|
|
702
|
+
latestComponentTimestamp = message.timestamp;
|
|
703
|
+
|
|
704
|
+
// Visible mode is intentionally untouched: Shift+Tab restores Pi's exact
|
|
705
|
+
// built-in Thinking Markdown renderer, including every OpenAI summary stage.
|
|
706
|
+
if (!self.hideThinkingBlock) {
|
|
707
|
+
originalUpdateContent.call(this, message, self.isStreaming);
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
if (activeThinking?.messageTimestamp === message.timestamp) {
|
|
712
|
+
streamingComponents.add(component);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
self.contentContainer.clear();
|
|
716
|
+
const hasActiveThinking =
|
|
717
|
+
activeThinking?.messageTimestamp === message.timestamp &&
|
|
718
|
+
message.content.some((content) => content.type === "thinking");
|
|
719
|
+
const hasVisibleContent =
|
|
720
|
+
hasActiveThinking ||
|
|
721
|
+
message.content.some(
|
|
722
|
+
(content) =>
|
|
723
|
+
(content.type === "text" && content.text.trim()) ||
|
|
724
|
+
(content.type === "thinking" && content.thinking.trim()),
|
|
725
|
+
);
|
|
726
|
+
// Reserve Pi's normal leading spacer even before the first thinking token.
|
|
727
|
+
// This prevents the placeholder heading from jumping down one row when
|
|
728
|
+
// summary/body content begins streaming.
|
|
729
|
+
if (hasVisibleContent) self.contentContainer.addChild(new Spacer(1));
|
|
730
|
+
|
|
731
|
+
for (let i = 0; i < message.content.length; i++) {
|
|
732
|
+
const content = message.content[i];
|
|
733
|
+
|
|
734
|
+
if (content.type === "text" && content.text.trim()) {
|
|
735
|
+
self.contentContainer.addChild(
|
|
736
|
+
new Markdown(
|
|
737
|
+
content.text.trim(),
|
|
738
|
+
self.outputPad,
|
|
739
|
+
0,
|
|
740
|
+
self.markdownTheme,
|
|
741
|
+
undefined,
|
|
742
|
+
{
|
|
743
|
+
transform: createTransform("assistant", self),
|
|
744
|
+
},
|
|
745
|
+
),
|
|
746
|
+
);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (content.type !== "thinking") continue;
|
|
751
|
+
|
|
752
|
+
const runStartIndex = i;
|
|
753
|
+
const hasVisibleContentBefore = message.content
|
|
754
|
+
.slice(0, runStartIndex)
|
|
755
|
+
.some(
|
|
756
|
+
(previous) =>
|
|
757
|
+
(previous.type === "text" && previous.text.trim()) ||
|
|
758
|
+
(previous.type === "thinking" && previous.thinking.trim()),
|
|
759
|
+
);
|
|
760
|
+
const thinkingBlocks: string[] = [];
|
|
761
|
+
let latestSummary: SummaryPart | undefined;
|
|
762
|
+
for (; i < message.content.length; i++) {
|
|
763
|
+
const thinkingContent = message.content[i];
|
|
764
|
+
if (thinkingContent.type !== "thinking") break;
|
|
765
|
+
const thinking = thinkingContent.thinking.trim();
|
|
766
|
+
if (!thinking) continue;
|
|
767
|
+
thinkingBlocks.push(thinking);
|
|
768
|
+
|
|
769
|
+
if (
|
|
770
|
+
config.useSummaryTitlesAsThinkingTitle &&
|
|
771
|
+
isOpenAiResponsesMessage(message)
|
|
772
|
+
) {
|
|
773
|
+
const contentIsActive =
|
|
774
|
+
activeThinking?.messageTimestamp === message.timestamp &&
|
|
775
|
+
activeThinking.contentIndex === i;
|
|
776
|
+
// During streaming, thinkingSignature may contain only the previously
|
|
777
|
+
// completed summary parts. Prefer the live text so a newly arriving
|
|
778
|
+
// title immediately replaces the old one instead of appearing in its
|
|
779
|
+
// preview body. Once complete, the structured signature is canonical.
|
|
780
|
+
latestSummary = contentIsActive
|
|
781
|
+
? (parseLatestStreamingSummary(thinking) ??
|
|
782
|
+
getLatestOpenAiSummary(thinkingContent.thinkingSignature) ??
|
|
783
|
+
latestSummary)
|
|
784
|
+
: (getLatestOpenAiSummary(thinkingContent.thinkingSignature) ??
|
|
785
|
+
parseLatestStreamingSummary(thinking) ??
|
|
786
|
+
latestSummary);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
const runEndIndex = i - 1;
|
|
790
|
+
i--;
|
|
791
|
+
const active = isActiveRun(message, runStartIndex, runEndIndex);
|
|
792
|
+
// OpenAI can spend several seconds reasoning before it emits the first
|
|
793
|
+
// summary token. Keep an empty active block visible as animated
|
|
794
|
+
// "Thinking..." during that otherwise silent interval.
|
|
795
|
+
if (thinkingBlocks.length === 0 && !active) continue;
|
|
796
|
+
if (hasVisibleContentBefore) {
|
|
797
|
+
self.contentContainer.addChild(new Spacer(1));
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
const elapsedMs = activeThinking
|
|
801
|
+
? Math.max(1, Date.now() - activeThinking.startedAt)
|
|
802
|
+
: undefined;
|
|
803
|
+
const completedMs = getCompletedDuration(
|
|
804
|
+
message.timestamp,
|
|
805
|
+
runStartIndex,
|
|
806
|
+
runEndIndex,
|
|
807
|
+
);
|
|
808
|
+
const durationMs = active ? elapsedMs : completedMs;
|
|
809
|
+
const durationText =
|
|
810
|
+
durationMs === undefined
|
|
811
|
+
? undefined
|
|
812
|
+
: formatThoughtDuration(durationMs);
|
|
813
|
+
|
|
814
|
+
let heading: string;
|
|
815
|
+
if (active && latestSummary) {
|
|
816
|
+
heading =
|
|
817
|
+
animatedText(latestSummary.title, summaryTitleStyle, true) +
|
|
818
|
+
(durationText ? thinkingStyle(` · ${durationText}`) : "");
|
|
819
|
+
} else if (active) {
|
|
820
|
+
const label = self.hiddenThinkingLabel || "Thinking...";
|
|
821
|
+
heading =
|
|
822
|
+
animatedText(label, thinkingStyle, true) +
|
|
823
|
+
(durationText ? thinkingStyle(` · ${durationText}`) : "");
|
|
824
|
+
} else {
|
|
825
|
+
// Completed compact blocks use one provider-independent status line.
|
|
826
|
+
// 无时长记录(中断流/旧会话/entry 缺失或索引错位)时不回退到加载文案
|
|
827
|
+
// "Thinking...":改用摘要标题或中性 "Thought"。
|
|
828
|
+
heading = thinkingStyle(
|
|
829
|
+
durationText
|
|
830
|
+
? `Thought for ${durationText}`
|
|
831
|
+
: (latestSummary?.title ?? "Thought"),
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
const previewSource = (
|
|
835
|
+
latestSummary?.body ?? thinkingBlocks.join("\n\n")
|
|
836
|
+
).trim();
|
|
837
|
+
self.contentContainer.addChild(
|
|
838
|
+
new ThinkingPreviewBlock(
|
|
839
|
+
heading,
|
|
840
|
+
previewSource,
|
|
841
|
+
self.outputPad,
|
|
842
|
+
message.timestamp,
|
|
843
|
+
thinkingStyle,
|
|
844
|
+
activeTheme,
|
|
845
|
+
runStartIndex,
|
|
846
|
+
),
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
const hasVisibleContentAfter = message.content
|
|
850
|
+
.slice(i + 1)
|
|
851
|
+
.some(
|
|
852
|
+
(next) =>
|
|
853
|
+
(next.type === "text" && next.text.trim()) ||
|
|
854
|
+
(next.type === "thinking" && next.thinking.trim()),
|
|
855
|
+
);
|
|
856
|
+
if (hasVisibleContentAfter) self.contentContainer.addChild(new Spacer(1));
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const hasToolCalls = message.content.some(
|
|
860
|
+
(content) => content.type === "toolCall",
|
|
861
|
+
);
|
|
862
|
+
self.hasToolCalls = hasToolCalls;
|
|
863
|
+
|
|
864
|
+
if (message.stopReason === "length") {
|
|
865
|
+
self.contentContainer.addChild(new Spacer(1));
|
|
866
|
+
self.contentContainer.addChild(
|
|
867
|
+
new Text(
|
|
868
|
+
activeTheme
|
|
869
|
+
? activeTheme.fg(
|
|
870
|
+
"error",
|
|
871
|
+
"Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.",
|
|
872
|
+
)
|
|
873
|
+
: "Error: Model stopped because it reached the maximum output token limit. The response may be incomplete.",
|
|
874
|
+
self.outputPad,
|
|
875
|
+
0,
|
|
876
|
+
),
|
|
877
|
+
);
|
|
878
|
+
} else if (!hasToolCalls) {
|
|
879
|
+
if (message.stopReason === "aborted") {
|
|
880
|
+
const abortMessage =
|
|
881
|
+
message.errorMessage && message.errorMessage !== "Request was aborted"
|
|
882
|
+
? message.errorMessage
|
|
883
|
+
: "Operation aborted";
|
|
884
|
+
self.contentContainer.addChild(new Spacer(1));
|
|
885
|
+
self.contentContainer.addChild(
|
|
886
|
+
new Text(
|
|
887
|
+
activeTheme ? activeTheme.fg("error", abortMessage) : abortMessage,
|
|
888
|
+
self.outputPad,
|
|
889
|
+
0,
|
|
890
|
+
),
|
|
891
|
+
);
|
|
892
|
+
} else if (message.stopReason === "error") {
|
|
893
|
+
const errorMessage = message.errorMessage || "Unknown error";
|
|
894
|
+
self.contentContainer.addChild(new Spacer(1));
|
|
895
|
+
self.contentContainer.addChild(
|
|
896
|
+
new Text(
|
|
897
|
+
activeTheme
|
|
898
|
+
? activeTheme.fg("error", `Error: ${errorMessage}`)
|
|
899
|
+
: `Error: ${errorMessage}`,
|
|
900
|
+
self.outputPad,
|
|
901
|
+
0,
|
|
902
|
+
),
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
};
|
|
907
|
+
const installedUpdateContent = prototype.updateContent;
|
|
908
|
+
(installedUpdateContent as any)[COMPACT_THINKING_PATCH_KEY] = true;
|
|
909
|
+
|
|
910
|
+
function ensureAnimationTimer() {
|
|
911
|
+
if (animationTimer) return;
|
|
912
|
+
animationTimer = setInterval(() => {
|
|
913
|
+
animationFrame++;
|
|
914
|
+
for (const component of streamingComponents) {
|
|
915
|
+
const self = component as unknown as AssistantInternals;
|
|
916
|
+
if (self.lastMessage) self.updateContent(self.lastMessage);
|
|
917
|
+
}
|
|
918
|
+
activeTui?.requestRender();
|
|
919
|
+
}, config.animationIntervalMs);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function stopAnimationTimerIfIdle() {
|
|
923
|
+
if (activeThinking || compactSummaryActive || !animationTimer) return;
|
|
924
|
+
clearInterval(animationTimer);
|
|
925
|
+
animationTimer = undefined;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
activeCompactSummarySetter = (active) => {
|
|
929
|
+
compactSummaryActive = active;
|
|
930
|
+
if (active) ensureAnimationTimer();
|
|
931
|
+
else stopAnimationTimerIfIdle();
|
|
932
|
+
activeTui?.requestRender();
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
function startThinking(message: AssistantMessage, contentIndex: number) {
|
|
936
|
+
activeThinking = {
|
|
937
|
+
messageTimestamp: message.timestamp,
|
|
938
|
+
contentIndex,
|
|
939
|
+
startedAt: Date.now(),
|
|
940
|
+
};
|
|
941
|
+
streamingComponents.clear();
|
|
942
|
+
animationFrame = 0;
|
|
943
|
+
|
|
944
|
+
// Depending on event-listener order, Pi may have rendered the empty
|
|
945
|
+
// thinking_start partial just before this extension receives the event.
|
|
946
|
+
// Re-render that component immediately so users do not wait for the first
|
|
947
|
+
// summary delta before seeing the animation.
|
|
948
|
+
if (latestComponent && latestComponentTimestamp === message.timestamp) {
|
|
949
|
+
streamingComponents.add(latestComponent);
|
|
950
|
+
const self = latestComponent as unknown as AssistantInternals;
|
|
951
|
+
self.updateContent(message);
|
|
952
|
+
activeTui?.requestRender();
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
if (animationTimer) clearInterval(animationTimer);
|
|
956
|
+
animationTimer = undefined;
|
|
957
|
+
ensureAnimationTimer();
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
function finishThinking() {
|
|
961
|
+
if (!activeThinking) return;
|
|
962
|
+
const finished = activeThinking;
|
|
963
|
+
const durationMs = Math.max(1, Date.now() - finished.startedAt);
|
|
964
|
+
let durations = completedDurations.get(finished.messageTimestamp);
|
|
965
|
+
if (!durations) {
|
|
966
|
+
durations = new Map();
|
|
967
|
+
completedDurations.set(finished.messageTimestamp, durations);
|
|
968
|
+
}
|
|
969
|
+
durations.set(finished.contentIndex, durationMs);
|
|
970
|
+
pi.appendEntry(DURATION_ENTRY_TYPE, {
|
|
971
|
+
messageTimestamp: finished.messageTimestamp,
|
|
972
|
+
contentIndex: finished.contentIndex,
|
|
973
|
+
durationMs,
|
|
974
|
+
} as DurationEntryData);
|
|
975
|
+
|
|
976
|
+
activeThinking = undefined;
|
|
977
|
+
stopAnimationTimerIfIdle();
|
|
978
|
+
|
|
979
|
+
const components = [...streamingComponents];
|
|
980
|
+
streamingComponents.clear();
|
|
981
|
+
for (const component of components) {
|
|
982
|
+
const self = component as unknown as AssistantInternals;
|
|
983
|
+
if (self.lastMessage) self.updateContent(self.lastMessage);
|
|
984
|
+
}
|
|
985
|
+
activeTui?.requestRender();
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// ---- fork patch:subagent 工具执行期间保持思考动画 ----
|
|
989
|
+
|
|
990
|
+
// Subagent tools can run for minutes: keep the thinking loading animation
|
|
991
|
+
// alive for the whole execution and only finalize once the tool ends or the
|
|
992
|
+
// model emits the next text/thinking boundary.
|
|
993
|
+
function resumeAgentThinking(message: AssistantMessage | undefined) {
|
|
994
|
+
if (activeThinking) return;
|
|
995
|
+
const content = message?.content;
|
|
996
|
+
if (!Array.isArray(content)) return;
|
|
997
|
+
const index = content.findIndex((item) => item?.type === "thinking");
|
|
998
|
+
if (index < 0) return;
|
|
999
|
+
startThinking(message as AssistantMessage, index);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
function messageHasAgentTool(message: AssistantMessage | undefined) {
|
|
1003
|
+
return (
|
|
1004
|
+
Array.isArray(message?.content) &&
|
|
1005
|
+
message.content.some(
|
|
1006
|
+
(content) =>
|
|
1007
|
+
content?.type === "toolCall" &&
|
|
1008
|
+
(content.name === "Agent" ||
|
|
1009
|
+
content.name === "Agents" ||
|
|
1010
|
+
content.arguments?.subagent_type != null),
|
|
1011
|
+
)
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
pi.on("message_update", (event) => {
|
|
1016
|
+
if (event.message.role !== "assistant") return;
|
|
1017
|
+
const update = event.assistantMessageEvent;
|
|
1018
|
+
|
|
1019
|
+
if (update.type === "thinking_start") {
|
|
1020
|
+
if (activeThinking?.messageTimestamp === event.message.timestamp) {
|
|
1021
|
+
// Some Responses-compatible providers emit a fresh thinking_start for
|
|
1022
|
+
// each summary/reasoning item even though no text or tool boundary has
|
|
1023
|
+
// ended the visible Thinking run. Follow the new content block without
|
|
1024
|
+
// resetting the run's original start time.
|
|
1025
|
+
activeThinking.contentIndex = update.contentIndex;
|
|
1026
|
+
} else {
|
|
1027
|
+
if (activeThinking) finishThinking();
|
|
1028
|
+
startThinking(event.message, update.contentIndex);
|
|
1029
|
+
}
|
|
1030
|
+
} else if (update.type === "thinking_delta") {
|
|
1031
|
+
if (!activeThinking) {
|
|
1032
|
+
startThinking(event.message, update.contentIndex);
|
|
1033
|
+
} else if (activeThinking.messageTimestamp === event.message.timestamp) {
|
|
1034
|
+
activeThinking.contentIndex = update.contentIndex;
|
|
1035
|
+
}
|
|
1036
|
+
} else if (update.type === "text_start") {
|
|
1037
|
+
finishThinking();
|
|
1038
|
+
} else if (
|
|
1039
|
+
update.type === "toolcall_start" ||
|
|
1040
|
+
update.type === "toolcall_delta"
|
|
1041
|
+
) {
|
|
1042
|
+
if (messageHasAgentTool(event.message)) {
|
|
1043
|
+
resumeAgentThinking(event.message);
|
|
1044
|
+
} else {
|
|
1045
|
+
finishThinking();
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
// Do not finalize on thinking_end alone. OpenAI Responses providers can
|
|
1049
|
+
// close one reasoning item and immediately open another for the next
|
|
1050
|
+
// summary while Pi still renders both as one contiguous Thinking run.
|
|
1051
|
+
// A text/tool transition or message_end is the actual visible boundary.
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
// OpenAI-compatible providers may not close reasoning until the response ends.
|
|
1055
|
+
pi.on("tool_execution_start", (event: any) => {
|
|
1056
|
+
if (event.toolName === "Agent" || event.toolName === "Agents") {
|
|
1057
|
+
const lastMessage = (
|
|
1058
|
+
latestComponent as unknown as { lastMessage?: AssistantMessage }
|
|
1059
|
+
)?.lastMessage;
|
|
1060
|
+
resumeAgentThinking(lastMessage);
|
|
1061
|
+
} else {
|
|
1062
|
+
finishThinking();
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
pi.on("tool_execution_end", (event: any) => {
|
|
1066
|
+
if (event.toolName === "Agent" || event.toolName === "Agents")
|
|
1067
|
+
finishThinking();
|
|
1068
|
+
});
|
|
1069
|
+
pi.on("message_end", (event) => {
|
|
1070
|
+
if (event.message.role !== "assistant") return;
|
|
1071
|
+
// Agent tool runs after message_end; keep the ticker until tool_execution_end
|
|
1072
|
+
// or the next text/thinking boundary.
|
|
1073
|
+
if (messageHasAgentTool(event.message)) return;
|
|
1074
|
+
finishThinking();
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1078
|
+
restoreDurationEntries(ctx.sessionManager.getBranch(), completedDurations);
|
|
1079
|
+
activeTheme = ctx.ui.theme;
|
|
1080
|
+
if (ctx.mode !== "tui") return;
|
|
1081
|
+
|
|
1082
|
+
// An empty widget gives the animation loop access to requestRender without
|
|
1083
|
+
// enabling terminal mouse reporting or intercepting native scrollback input.
|
|
1084
|
+
// setWidget 的 factory 同步执行,此刻补丁已安装。
|
|
1085
|
+
ctx.ui.setWidget(WIDGET_ID, (tui) => {
|
|
1086
|
+
activeTui = tui;
|
|
1087
|
+
return { render: () => [], invalidate() {} };
|
|
1088
|
+
});
|
|
1089
|
+
|
|
1090
|
+
// pi 在 reload/resume 时先于 session_start 用原始原型重建聊天组件
|
|
1091
|
+
// (rebuildChatFromMessages / renderCurrentSessionState)。由共享的
|
|
1092
|
+
// refreshMountedTranscript 扫描挂载树重绘这些组件,恢复 compact 渲染、
|
|
1093
|
+
// 持久化时长与工具调用显示。
|
|
1094
|
+
refreshMountedTranscript(activeTui);
|
|
1095
|
+
activeTui?.requestRender(true);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
1099
|
+
restoreDurationEntries(ctx.sessionManager.getBranch(), completedDurations);
|
|
1100
|
+
refreshMountedTranscript(activeTui);
|
|
1101
|
+
activeTui?.requestRender(true);
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
1105
|
+
finishThinking();
|
|
1106
|
+
activeThinkingQuery = undefined;
|
|
1107
|
+
activeThinkingStateQuery = undefined;
|
|
1108
|
+
activeThinkingAnimationFrameQuery = undefined;
|
|
1109
|
+
activeCompactSummarySetter = undefined;
|
|
1110
|
+
compactSummaryActive = false;
|
|
1111
|
+
if (animationTimer) clearInterval(animationTimer);
|
|
1112
|
+
animationTimer = undefined;
|
|
1113
|
+
activeTui = undefined;
|
|
1114
|
+
activeTheme = undefined;
|
|
1115
|
+
latestComponent = undefined;
|
|
1116
|
+
latestComponentTimestamp = undefined;
|
|
1117
|
+
completedDurations.clear();
|
|
1118
|
+
streamingComponents.clear();
|
|
1119
|
+
expandedThinking.clear();
|
|
1120
|
+
clearThinkingPreviewCache();
|
|
1121
|
+
if (ctx.mode === "tui") ctx.ui.setWidget(WIDGET_ID, undefined);
|
|
1122
|
+
|
|
1123
|
+
if (patchInstalled) {
|
|
1124
|
+
if (prototype.updateContent === installedUpdateContent) {
|
|
1125
|
+
const wrappedOriginal = (originalUpdateContent as any)[
|
|
1126
|
+
PROTOTYPE_ORIGINAL_KEY
|
|
1127
|
+
];
|
|
1128
|
+
prototype.updateContent =
|
|
1129
|
+
typeof wrappedOriginal === "function"
|
|
1130
|
+
? wrappedOriginal
|
|
1131
|
+
: originalUpdateContent;
|
|
1132
|
+
}
|
|
1133
|
+
patchInstalled = false;
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
type CompactThinkingOwner = {
|
|
1139
|
+
owner: object;
|
|
1140
|
+
stop(event?: any, ctx?: any): void;
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
type UpstreamHandler = (event: any, ctx: any) => void;
|
|
1144
|
+
|
|
1145
|
+
export function installCompactThinking(
|
|
1146
|
+
pi: ExtensionAPI,
|
|
1147
|
+
initialConfig: CompactThinkingConfig,
|
|
1148
|
+
): CompactThinkingController {
|
|
1149
|
+
const owner = {};
|
|
1150
|
+
let session: { event: any; ctx: any } | undefined;
|
|
1151
|
+
let active = false;
|
|
1152
|
+
// Stable pi.on wrappers delegate here so activate/reload never double-binds.
|
|
1153
|
+
const delegates = new Map<string, UpstreamHandler>();
|
|
1154
|
+
const boundEvents = new Set<string>();
|
|
1155
|
+
|
|
1156
|
+
const restoreAllDurations = (ctx: any): any => {
|
|
1157
|
+
const sessionManager = ctx?.sessionManager;
|
|
1158
|
+
if (!sessionManager || typeof sessionManager.getEntries !== "function")
|
|
1159
|
+
return ctx;
|
|
1160
|
+
return {
|
|
1161
|
+
...ctx,
|
|
1162
|
+
sessionManager: {
|
|
1163
|
+
...sessionManager,
|
|
1164
|
+
getBranch: () => sessionManager.getEntries(),
|
|
1165
|
+
},
|
|
1166
|
+
};
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
const bind = (eventName: string) => {
|
|
1170
|
+
if (boundEvents.has(eventName)) return;
|
|
1171
|
+
boundEvents.add(eventName);
|
|
1172
|
+
pi.on(eventName as any, (e: any, ctx: any) => {
|
|
1173
|
+
if (!active) return;
|
|
1174
|
+
const handler = delegates.get(eventName);
|
|
1175
|
+
if (!handler) return;
|
|
1176
|
+
handler(e, eventName === "session_tree" ? restoreAllDurations(ctx) : ctx);
|
|
1177
|
+
});
|
|
1178
|
+
};
|
|
1179
|
+
|
|
1180
|
+
const stop = (event?: any, ctx?: any) => {
|
|
1181
|
+
if (!active) return;
|
|
1182
|
+
active = false;
|
|
1183
|
+
const shutdown = delegates.get("session_shutdown");
|
|
1184
|
+
delegates.clear();
|
|
1185
|
+
shutdown?.(
|
|
1186
|
+
event ?? session?.event ?? {},
|
|
1187
|
+
ctx ?? session?.ctx ?? { mode: "rpc", ui: {} },
|
|
1188
|
+
);
|
|
1189
|
+
if (
|
|
1190
|
+
patchRegistry.get<CompactThinkingOwner>(COMPACT_THINKING_OWNER)?.owner ===
|
|
1191
|
+
owner
|
|
1192
|
+
)
|
|
1193
|
+
patchRegistry.delete(COMPACT_THINKING_OWNER);
|
|
1194
|
+
};
|
|
1195
|
+
|
|
1196
|
+
const activate = (event: any, ctx: any) => {
|
|
1197
|
+
// Headless subagent runtimes share this process. Never steal the parent
|
|
1198
|
+
// TUI prototype patch or kill its thinking ticker.
|
|
1199
|
+
if (ctx?.mode !== "tui") return;
|
|
1200
|
+
|
|
1201
|
+
patchRegistry
|
|
1202
|
+
.get<CompactThinkingOwner>(COMPACT_THINKING_OWNER)
|
|
1203
|
+
?.stop(event, ctx);
|
|
1204
|
+
session = { event, ctx };
|
|
1205
|
+
|
|
1206
|
+
// 配置统一由 claude-code-style 管控,加载时覆盖库默认值。
|
|
1207
|
+
Object.assign(config, initialConfig);
|
|
1208
|
+
delegates.clear();
|
|
1209
|
+
|
|
1210
|
+
compactThinking({
|
|
1211
|
+
on(eventName: string, handler: UpstreamHandler) {
|
|
1212
|
+
if (eventName === "session_start") {
|
|
1213
|
+
// Already inside session_start — run immediately.
|
|
1214
|
+
handler(event, restoreAllDurations(ctx));
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
if (eventName === "session_shutdown") {
|
|
1218
|
+
delegates.set(eventName, handler);
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
delegates.set(eventName, handler);
|
|
1222
|
+
bind(eventName);
|
|
1223
|
+
},
|
|
1224
|
+
appendEntry: (...args: any[]) => (pi.appendEntry as any)(...args),
|
|
1225
|
+
} as unknown as ExtensionAPI);
|
|
1226
|
+
|
|
1227
|
+
active = true;
|
|
1228
|
+
patchRegistry.install(COMPACT_THINKING_OWNER, { owner, stop });
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
pi.on("session_start", (event, ctx) => {
|
|
1232
|
+
session = { event, ctx };
|
|
1233
|
+
activate(event, ctx);
|
|
1234
|
+
});
|
|
1235
|
+
pi.on("session_shutdown", (event, ctx) => {
|
|
1236
|
+
if (
|
|
1237
|
+
patchRegistry.get<CompactThinkingOwner>(COMPACT_THINKING_OWNER)?.owner ===
|
|
1238
|
+
owner
|
|
1239
|
+
)
|
|
1240
|
+
stop(event, ctx);
|
|
1241
|
+
session = undefined;
|
|
1242
|
+
});
|
|
1243
|
+
|
|
1244
|
+
return {
|
|
1245
|
+
updateConfig(next) {
|
|
1246
|
+
Object.assign(initialConfig, next);
|
|
1247
|
+
Object.assign(config, next);
|
|
1248
|
+
},
|
|
1249
|
+
getMessageThinkingDurationMs(messageTimestamp) {
|
|
1250
|
+
return activeThinkingQuery?.(messageTimestamp);
|
|
1251
|
+
},
|
|
1252
|
+
isMessageThinkingActive(messageTimestamp) {
|
|
1253
|
+
return activeThinkingStateQuery?.(messageTimestamp) ?? false;
|
|
1254
|
+
},
|
|
1255
|
+
getThinkingAnimationFrame() {
|
|
1256
|
+
return activeThinkingAnimationFrameQuery?.() ?? 0;
|
|
1257
|
+
},
|
|
1258
|
+
setCompactSummaryActive(active) {
|
|
1259
|
+
activeCompactSummarySetter?.(active);
|
|
1260
|
+
},
|
|
1261
|
+
};
|
|
1262
|
+
}
|