pi-my-compact-ui 0.2.2 → 0.3.0
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.md +31 -3
- package/index.ts +3 -1744
- package/package.json +5 -3
- package/src/animation.ts +37 -0
- package/src/assistant-patches.ts +431 -0
- package/src/compaction.ts +75 -0
- package/src/config.ts +31 -0
- package/src/constants.ts +36 -0
- package/src/extension.ts +282 -0
- package/src/grouping.ts +327 -0
- package/src/guards.ts +58 -0
- package/src/helpers.ts +130 -0
- package/src/markdown.ts +98 -0
- package/src/modals.ts +579 -0
- package/src/settings.ts +142 -0
- package/src/state.ts +80 -0
- package/src/streaming-tools.ts +232 -0
- package/src/thinking.ts +62 -0
- package/src/tool-group.ts +449 -0
- package/src/types.ts +52 -0
package/src/settings.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/** 配置交互界面与数值编辑器。 */
|
|
2
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Key, SettingsList, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
import type { Component, SettingItem } from "@earendil-works/pi-tui";
|
|
6
|
+
import { config, saveConfig } from "./config.js";
|
|
7
|
+
import { runtime } from "./state.js";
|
|
8
|
+
|
|
9
|
+
export const CONFIG_KEYS = [
|
|
10
|
+
{
|
|
11
|
+
id: "expandedToolLines",
|
|
12
|
+
label: "Expanded tool lines",
|
|
13
|
+
description: "Result lines shown per tool when expanded",
|
|
14
|
+
min: 1,
|
|
15
|
+
max: 50,
|
|
16
|
+
step: 1,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "expandedThinkingLines",
|
|
20
|
+
label: "Expanded thinking lines",
|
|
21
|
+
description: "Thinking lines shown when expanded",
|
|
22
|
+
min: 1,
|
|
23
|
+
max: 100,
|
|
24
|
+
step: 1,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "maxGroupEntries",
|
|
28
|
+
label: "Max collapsed entries",
|
|
29
|
+
description: "Maximum entries shown in collapsed group",
|
|
30
|
+
min: 1,
|
|
31
|
+
max: 30,
|
|
32
|
+
step: 1,
|
|
33
|
+
},
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
export function makeStepper(
|
|
37
|
+
title: string,
|
|
38
|
+
initial: number,
|
|
39
|
+
meta: { min: number; max: number; step: number },
|
|
40
|
+
theme: any,
|
|
41
|
+
done: (value?: string) => void,
|
|
42
|
+
): Component {
|
|
43
|
+
let value = initial;
|
|
44
|
+
let cachedWidth: number | undefined;
|
|
45
|
+
let cachedLines: string[] | undefined;
|
|
46
|
+
const fg = (color: string, t: string) => theme?.fg?.(color, t) ?? t;
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
render(width: number): string[] {
|
|
50
|
+
if (cachedLines && cachedWidth === width) return cachedLines;
|
|
51
|
+
const barLen = Math.max(1, Math.min(width - 10, 40));
|
|
52
|
+
const ratio = (value - meta.min) / Math.max(1, meta.max - meta.min);
|
|
53
|
+
const filled = Math.round(ratio * barLen);
|
|
54
|
+
const bar = "█".repeat(filled) + "░".repeat(Math.max(0, barLen - filled));
|
|
55
|
+
const titleText = theme?.bold ? theme.bold(title) : title;
|
|
56
|
+
cachedLines = [
|
|
57
|
+
fg("accent", titleText),
|
|
58
|
+
"",
|
|
59
|
+
` ${fg("accent", String(value))}`,
|
|
60
|
+
` ${fg("muted", bar)}`,
|
|
61
|
+
"",
|
|
62
|
+
fg("dim", " ◀ ▶ / − + adjust Enter save Esc cancel"),
|
|
63
|
+
].map((line) => truncateToWidth(line, Math.max(1, width)));
|
|
64
|
+
cachedWidth = width;
|
|
65
|
+
return cachedLines;
|
|
66
|
+
},
|
|
67
|
+
handleInput(data: string): void {
|
|
68
|
+
if (matchesKey(data, Key.left) || matchesKey(data, Key.down) || data === "-" || data === "_") {
|
|
69
|
+
value = Math.max(meta.min, value - meta.step);
|
|
70
|
+
} else if (matchesKey(data, Key.right) || matchesKey(data, Key.up) || data === "+" || data === "=") {
|
|
71
|
+
value = Math.min(meta.max, value + meta.step);
|
|
72
|
+
} else if (matchesKey(data, Key.enter) || matchesKey(data, Key.space)) {
|
|
73
|
+
done(String(value));
|
|
74
|
+
return;
|
|
75
|
+
} else if (matchesKey(data, Key.escape)) {
|
|
76
|
+
done(undefined);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
cachedWidth = undefined;
|
|
80
|
+
},
|
|
81
|
+
invalidate(): void {
|
|
82
|
+
cachedWidth = undefined;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function registerConfigCommand(pi: ExtensionAPI): void {
|
|
88
|
+
pi.registerCommand("compact-ui-config", {
|
|
89
|
+
description: "Interactive compact-ui settings (arrows to select, Enter to adjust, Esc to close)",
|
|
90
|
+
handler: async (_args, ctx) => {
|
|
91
|
+
// Non-TUI modes (print/json) can't show the interactive menu.
|
|
92
|
+
if (!ctx.hasUI) {
|
|
93
|
+
ctx.ui.notify(
|
|
94
|
+
`compact: expandedToolLines=${config.expandedToolLines}, expandedThinkingLines=${config.expandedThinkingLines}`,
|
|
95
|
+
"info",
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const changed = await ctx.ui.custom<boolean>((tui, theme, _keybindings, done) => {
|
|
101
|
+
let anyChanged = false;
|
|
102
|
+
const items: SettingItem[] = CONFIG_KEYS.map((meta) => ({
|
|
103
|
+
id: meta.id,
|
|
104
|
+
label: meta.label,
|
|
105
|
+
currentValue: String((config as any)[meta.id]),
|
|
106
|
+
description: meta.description,
|
|
107
|
+
submenu: (currentValue: string, subDone: (value?: string) => void) =>
|
|
108
|
+
makeStepper(meta.label, Number(currentValue), meta, theme, subDone),
|
|
109
|
+
}));
|
|
110
|
+
const settingsList = new SettingsList(
|
|
111
|
+
items,
|
|
112
|
+
Math.min(items.length, 15),
|
|
113
|
+
getSettingsListTheme(),
|
|
114
|
+
(id, newValue) => {
|
|
115
|
+
// Persist and refresh the live groups when SettingsList commits a change.
|
|
116
|
+
(config as any)[id] = Number(newValue);
|
|
117
|
+
saveConfig();
|
|
118
|
+
anyChanged = true;
|
|
119
|
+
for (const g of runtime.groups) g.invalidate();
|
|
120
|
+
},
|
|
121
|
+
() => done(anyChanged),
|
|
122
|
+
);
|
|
123
|
+
return {
|
|
124
|
+
render(width: number) {
|
|
125
|
+
return settingsList.render(width);
|
|
126
|
+
},
|
|
127
|
+
invalidate() {
|
|
128
|
+
settingsList.invalidate();
|
|
129
|
+
},
|
|
130
|
+
handleInput(data: string) {
|
|
131
|
+
settingsList.handleInput?.(data);
|
|
132
|
+
tui.requestRender();
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (changed) {
|
|
138
|
+
ctx.ui.notify("compact-ui settings saved", "info");
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/** 集中管理会话状态;集合与锚点通过工厂创建,便于测试隔离。 */
|
|
2
|
+
import type { ThinkingEntry, PreparingTool, AssistantContentState } from "./types.js";
|
|
3
|
+
import type { ToolGroupComponent } from "./tool-group.js";
|
|
4
|
+
import type { OverlayHandle, Container } from "@earendil-works/pi-tui";
|
|
5
|
+
import type { DetailModalComponent, InspectSelectorModal } from "./modals.js";
|
|
6
|
+
|
|
7
|
+
/** Mutable session/UI state. A fresh snapshot also makes isolated tests possible. */
|
|
8
|
+
export interface RuntimeState {
|
|
9
|
+
currentTheme: any;
|
|
10
|
+
activeThinking: ThinkingEntry | null;
|
|
11
|
+
activeThinkingIndex: number | null;
|
|
12
|
+
activeThinkingBlocks: Map<number, string>;
|
|
13
|
+
unattachedThinking: ThinkingEntry[];
|
|
14
|
+
thinkingEntrySeq: number;
|
|
15
|
+
handledTextIndexes: Set<number>;
|
|
16
|
+
sealedThinkingIndexes: Set<number>;
|
|
17
|
+
pendingTextSeal: boolean;
|
|
18
|
+
pendingTextOrdinal: number | null;
|
|
19
|
+
lastActiveGroup: ToolGroupComponent | null;
|
|
20
|
+
lastSealedThinkingEntry: ThinkingEntry | null;
|
|
21
|
+
lastReportedReasoningTokens: number;
|
|
22
|
+
lastStreamingComp: any;
|
|
23
|
+
lastChatContainer: any;
|
|
24
|
+
toolStarts: Map<string, number>;
|
|
25
|
+
toolDurations: Map<string, number>;
|
|
26
|
+
turnStartMs: number;
|
|
27
|
+
preparingByIndex: Map<number, PreparingTool>;
|
|
28
|
+
activeModalHandle: OverlayHandle | null;
|
|
29
|
+
activeModalComponent: DetailModalComponent | null;
|
|
30
|
+
activeModalCloseFn: (() => void) | null;
|
|
31
|
+
activeUIContext: any;
|
|
32
|
+
activeSelectorHandle: OverlayHandle | null;
|
|
33
|
+
activeSelectorComponent: InspectSelectorModal | null;
|
|
34
|
+
activeSelectorCloseFn: (() => void) | null;
|
|
35
|
+
animTimer: ReturnType<typeof setTimeout> | null;
|
|
36
|
+
capturedTui: any;
|
|
37
|
+
groups: Set<ToolGroupComponent>;
|
|
38
|
+
assistantContentContainers: WeakSet<Container>;
|
|
39
|
+
assistantContentStates: WeakMap<Container, AssistantContentState>;
|
|
40
|
+
groupAnchors: WeakMap<ToolGroupComponent, { container: Container; ordinal: number; }>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createRuntimeState(): RuntimeState {
|
|
44
|
+
return {
|
|
45
|
+
currentTheme: null,
|
|
46
|
+
activeThinking: null,
|
|
47
|
+
activeThinkingIndex: null,
|
|
48
|
+
activeThinkingBlocks: new Map<number, string>(),
|
|
49
|
+
unattachedThinking: [],
|
|
50
|
+
thinkingEntrySeq: 0,
|
|
51
|
+
handledTextIndexes: new Set<number>(),
|
|
52
|
+
sealedThinkingIndexes: new Set<number>(),
|
|
53
|
+
pendingTextSeal: false,
|
|
54
|
+
pendingTextOrdinal: null,
|
|
55
|
+
lastActiveGroup: null,
|
|
56
|
+
lastSealedThinkingEntry: null,
|
|
57
|
+
lastReportedReasoningTokens: 0,
|
|
58
|
+
lastStreamingComp: null,
|
|
59
|
+
lastChatContainer: null,
|
|
60
|
+
toolStarts: new Map<string, number>(),
|
|
61
|
+
toolDurations: new Map<string, number>(),
|
|
62
|
+
turnStartMs: 0,
|
|
63
|
+
preparingByIndex: new Map<number, PreparingTool>(),
|
|
64
|
+
activeModalHandle: null,
|
|
65
|
+
activeModalComponent: null,
|
|
66
|
+
activeModalCloseFn: null,
|
|
67
|
+
activeUIContext: null,
|
|
68
|
+
activeSelectorHandle: null,
|
|
69
|
+
activeSelectorComponent: null,
|
|
70
|
+
activeSelectorCloseFn: null,
|
|
71
|
+
animTimer: null,
|
|
72
|
+
capturedTui: null,
|
|
73
|
+
groups: new Set<ToolGroupComponent>(),
|
|
74
|
+
assistantContentContainers: new WeakSet<Container>(),
|
|
75
|
+
assistantContentStates: new WeakMap<Container, AssistantContentState>(),
|
|
76
|
+
groupAnchors: new WeakMap<ToolGroupComponent, { container: Container; ordinal: number }>(),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const runtime = createRuntimeState();
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/** 在参数流式输出阶段创建工具占位行,并迁移到真实工具组件。 */
|
|
2
|
+
import { runtime } from "./state.js";
|
|
3
|
+
import { PARENT_KEY } from "./constants.js";
|
|
4
|
+
import { isToolGroup } from "./guards.js";
|
|
5
|
+
import { ToolGroupComponent } from "./tool-group.js";
|
|
6
|
+
import { detachGroup, noteGroupMount } from "./assistant-patches.js";
|
|
7
|
+
import type { PreparingTool } from "./types.js";
|
|
8
|
+
import { locateStreamingToolCall, streamingToolArgs } from "./helpers.js";
|
|
9
|
+
|
|
10
|
+
export function toolElapsed(tool: any): string {
|
|
11
|
+
if (typeof tool?._elapsedDuration === "number") {
|
|
12
|
+
return tool._elapsedDuration.toFixed(1);
|
|
13
|
+
}
|
|
14
|
+
const knownDuration = runtime.toolDurations.get(tool.toolCallId);
|
|
15
|
+
if (knownDuration !== undefined) {
|
|
16
|
+
tool._elapsedDuration = knownDuration;
|
|
17
|
+
return knownDuration.toFixed(1);
|
|
18
|
+
}
|
|
19
|
+
if (tool?._groupEndAt !== undefined) {
|
|
20
|
+
const start = runtime.toolStarts.get(tool.toolCallId) ?? tool._groupEndAt;
|
|
21
|
+
const dur = Math.max(0, (tool._groupEndAt - start) / 1000);
|
|
22
|
+
tool._elapsedDuration = dur;
|
|
23
|
+
return dur.toFixed(1);
|
|
24
|
+
}
|
|
25
|
+
if (tool?.result) {
|
|
26
|
+
const resultTs = Number(tool.result?.timestamp);
|
|
27
|
+
const start = runtime.toolStarts.get(tool.toolCallId);
|
|
28
|
+
if (start && resultTs && resultTs >= start) {
|
|
29
|
+
const dur = (resultTs - start) / 1000;
|
|
30
|
+
tool._elapsedDuration = dur;
|
|
31
|
+
return dur.toFixed(1);
|
|
32
|
+
}
|
|
33
|
+
tool._elapsedDuration = 0;
|
|
34
|
+
return "0.0";
|
|
35
|
+
}
|
|
36
|
+
const start = runtime.toolStarts.get(tool.toolCallId);
|
|
37
|
+
if (start !== undefined) {
|
|
38
|
+
return Math.max(0, (Date.now() - start) / 1000).toFixed(1);
|
|
39
|
+
}
|
|
40
|
+
return "0.0";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function rememberToolStart(toolCallId: string): void {
|
|
44
|
+
if (!toolCallId || runtime.toolStarts.has(toolCallId) || runtime.toolDurations.has(toolCallId)) return;
|
|
45
|
+
runtime.toolStarts.set(toolCallId, Date.now());
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function loadSessionToolTimings(entries: readonly any[]): void {
|
|
49
|
+
if (!Array.isArray(entries)) return;
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (entry?.type === "message" && entry.message) {
|
|
52
|
+
const msg = entry.message;
|
|
53
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
54
|
+
const startTime = Number(msg.timestamp) || (entry.timestamp ? new Date(entry.timestamp).getTime() : 0);
|
|
55
|
+
for (const block of msg.content) {
|
|
56
|
+
if (block.type === "toolCall" && block.id) {
|
|
57
|
+
if (startTime > 0) runtime.toolStarts.set(block.id, startTime);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
} else if (msg.role === "toolResult" && msg.toolCallId) {
|
|
61
|
+
const endTime = Number(msg.timestamp) || (entry.timestamp ? new Date(entry.timestamp).getTime() : 0);
|
|
62
|
+
const startTime = runtime.toolStarts.get(msg.toolCallId);
|
|
63
|
+
if (startTime && endTime >= startTime) {
|
|
64
|
+
runtime.toolDurations.set(msg.toolCallId, (endTime - startTime) / 1000);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function findGroupedTool(toolCallId: string, contentIndex: number): any | undefined {
|
|
72
|
+
for (const group of runtime.groups) {
|
|
73
|
+
for (const entry of group.entries) {
|
|
74
|
+
if (entry.kind !== "tool") continue;
|
|
75
|
+
const tool = entry.tool;
|
|
76
|
+
if (toolCallId && tool?.toolCallId === toolCallId) return tool;
|
|
77
|
+
if (tool?._contentIndex === contentIndex) return tool;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function invalidateGroupedTool(tool: any): void {
|
|
84
|
+
const group = tool?.owner ?? tool?.[PARENT_KEY];
|
|
85
|
+
if (isToolGroup(group)) {
|
|
86
|
+
if (!group.sealed) runtime.lastActiveGroup = group;
|
|
87
|
+
group.invalidate();
|
|
88
|
+
} else {
|
|
89
|
+
runtime.lastActiveGroup?.invalidate();
|
|
90
|
+
}
|
|
91
|
+
runtime.capturedTui?.requestRender?.();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function findOpenGroup(): ToolGroupComponent | null {
|
|
95
|
+
if (runtime.lastActiveGroup && !runtime.lastActiveGroup.sealed && runtime.groups.has(runtime.lastActiveGroup)) return runtime.lastActiveGroup;
|
|
96
|
+
for (const group of runtime.groups) {
|
|
97
|
+
if (!group.sealed) return group;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function placeGroupAfter(parent: any, after: any, group: ToolGroupComponent): void {
|
|
103
|
+
const children = parent?.children;
|
|
104
|
+
if (!Array.isArray(children)) return;
|
|
105
|
+
detachGroup(group, parent);
|
|
106
|
+
const existing = children.indexOf(group);
|
|
107
|
+
if (existing >= 0) children.splice(existing, 1);
|
|
108
|
+
const afterIndex = after ? children.indexOf(after) : -1;
|
|
109
|
+
children.splice(afterIndex >= 0 ? afterIndex + 1 : children.length, 0, group);
|
|
110
|
+
noteGroupMount(parent, group);
|
|
111
|
+
runtime.lastActiveGroup = group;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function openToolGroup(): ToolGroupComponent | null {
|
|
115
|
+
const open = findOpenGroup();
|
|
116
|
+
if (open) {
|
|
117
|
+
if (runtime.lastChatContainer && runtime.lastStreamingComp) placeGroupAfter(runtime.lastChatContainer, runtime.lastStreamingComp, open);
|
|
118
|
+
return open;
|
|
119
|
+
}
|
|
120
|
+
if (!runtime.lastChatContainer || !runtime.lastStreamingComp) return null;
|
|
121
|
+
let group: ToolGroupComponent;
|
|
122
|
+
const parent = runtime.lastChatContainer;
|
|
123
|
+
const children = parent.children;
|
|
124
|
+
if (!Array.isArray(children)) return null;
|
|
125
|
+
const idx = children.indexOf(runtime.lastStreamingComp);
|
|
126
|
+
group = new ToolGroupComponent();
|
|
127
|
+
detachGroup(group);
|
|
128
|
+
children.splice(idx >= 0 ? idx + 1 : children.length, 0, group);
|
|
129
|
+
noteGroupMount(parent, group);
|
|
130
|
+
runtime.groups.add(group);
|
|
131
|
+
runtime.lastActiveGroup = group;
|
|
132
|
+
parent.invalidate?.();
|
|
133
|
+
return group;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function attachPreparingTool(tool: PreparingTool): void {
|
|
137
|
+
if (tool.owner) return;
|
|
138
|
+
const group = openToolGroup();
|
|
139
|
+
if (!group) return;
|
|
140
|
+
tool.owner = group;
|
|
141
|
+
group.entries.push({ kind: "tool", tool });
|
|
142
|
+
group.invalidate();
|
|
143
|
+
runtime.lastActiveGroup = group;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function detachPreparingTool(tool: PreparingTool): void {
|
|
147
|
+
const group = tool.owner;
|
|
148
|
+
if (group) {
|
|
149
|
+
const index = group.entries.findIndex((entry) => entry.kind === "tool" && entry.tool === tool);
|
|
150
|
+
if (index >= 0) group.entries.splice(index, 1);
|
|
151
|
+
}
|
|
152
|
+
tool.owner = null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function takePreparingTool(real: any): PreparingTool | undefined {
|
|
156
|
+
const id = String(real?.toolCallId ?? "");
|
|
157
|
+
const name = String(real?.toolName ?? "");
|
|
158
|
+
if (id) {
|
|
159
|
+
for (const [index, candidate] of runtime.preparingByIndex) {
|
|
160
|
+
if (candidate.toolCallId === id) {
|
|
161
|
+
runtime.preparingByIndex.delete(index);
|
|
162
|
+
return candidate;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const matches = [...runtime.preparingByIndex.entries()].filter(([, candidate]) => candidate.toolName === name);
|
|
167
|
+
if (matches.length === 1) {
|
|
168
|
+
runtime.preparingByIndex.delete(matches[0][0]);
|
|
169
|
+
return matches[0][1];
|
|
170
|
+
}
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function absorbPreparingTool(real: any): void {
|
|
175
|
+
const preparing = takePreparingTool(real);
|
|
176
|
+
if (!preparing) return;
|
|
177
|
+
const id = String(real?.toolCallId ?? "");
|
|
178
|
+
const start = runtime.toolStarts.get(preparing.toolCallId);
|
|
179
|
+
if (start !== undefined && id && !runtime.toolStarts.has(id)) runtime.toolStarts.set(id, start);
|
|
180
|
+
real._contentIndex = preparing._contentIndex;
|
|
181
|
+
if ((!real.args || Object.keys(real.args).length === 0) && preparing.args) real.args = preparing.args;
|
|
182
|
+
detachPreparingTool(preparing);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function clearPreparingTools(): void {
|
|
186
|
+
for (const tool of runtime.preparingByIndex.values()) detachPreparingTool(tool);
|
|
187
|
+
runtime.preparingByIndex.clear();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function noteStreamingToolCall(content: any[], streamEvent: any): void {
|
|
191
|
+
const located = locateStreamingToolCall(content, streamEvent);
|
|
192
|
+
if (!located) return;
|
|
193
|
+
const { index, block } = located;
|
|
194
|
+
const id = String(block.id || "");
|
|
195
|
+
const name = String(block.name || "tool");
|
|
196
|
+
const args = streamingToolArgs(block);
|
|
197
|
+
const existing = findGroupedTool(id, index);
|
|
198
|
+
if (existing && !existing._preparing) {
|
|
199
|
+
existing._contentIndex = index;
|
|
200
|
+
existing.args = args;
|
|
201
|
+
rememberToolStart(String(existing.toolCallId || id));
|
|
202
|
+
invalidateGroupedTool(existing);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
let preparing = runtime.preparingByIndex.get(index);
|
|
207
|
+
if (!preparing) {
|
|
208
|
+
preparing = {
|
|
209
|
+
toolName: name,
|
|
210
|
+
toolCallId: id || `preparing:${index}`,
|
|
211
|
+
args,
|
|
212
|
+
isPartial: true,
|
|
213
|
+
_preparing: true,
|
|
214
|
+
_contentIndex: index,
|
|
215
|
+
owner: null,
|
|
216
|
+
};
|
|
217
|
+
runtime.preparingByIndex.set(index, preparing);
|
|
218
|
+
attachPreparingTool(preparing);
|
|
219
|
+
} else {
|
|
220
|
+
if (name && name !== "tool") preparing.toolName = name;
|
|
221
|
+
if (id && preparing.toolCallId !== id) {
|
|
222
|
+
const previous = preparing.toolCallId;
|
|
223
|
+
preparing.toolCallId = id;
|
|
224
|
+
const start = runtime.toolStarts.get(previous);
|
|
225
|
+
if (start !== undefined && !runtime.toolStarts.has(id)) runtime.toolStarts.set(id, start);
|
|
226
|
+
}
|
|
227
|
+
preparing.args = args;
|
|
228
|
+
if (!preparing.owner) attachPreparingTool(preparing);
|
|
229
|
+
}
|
|
230
|
+
rememberToolStart(preparing.toolCallId);
|
|
231
|
+
invalidateGroupedTool(preparing);
|
|
232
|
+
}
|
package/src/thinking.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/** 思考段生命周期与估算/精确 token 用量更新。 */
|
|
2
|
+
import { runtime } from "./state.js";
|
|
3
|
+
import { estimateTextTokens } from "./helpers.js";
|
|
4
|
+
import type { ThinkingEntry } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export function updateActiveThinkingTokens(message: any): void {
|
|
7
|
+
const entry = runtime.activeThinking;
|
|
8
|
+
if (!entry) return;
|
|
9
|
+
entry.text = [...runtime.activeThinkingBlocks.values()].filter((text) => text.trim()).join("\n\n");
|
|
10
|
+
// Most providers report reasoning usage only when the response finishes. While
|
|
11
|
+
// streaming, fall back to pi's own chars/4 token heuristic and mark it with ≈.
|
|
12
|
+
const reported = Number(message?.usage?.reasoning);
|
|
13
|
+
if (Number.isFinite(reported) && reported > 0) {
|
|
14
|
+
entry.tokens = reported;
|
|
15
|
+
entry.tokensExact = true;
|
|
16
|
+
runtime.lastReportedReasoningTokens = reported;
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
entry.tokens = estimateTextTokens(entry.text);
|
|
20
|
+
entry.tokensExact = false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createThinkingEntry(): ThinkingEntry {
|
|
24
|
+
const entry: ThinkingEntry = {
|
|
25
|
+
kind: "thinking",
|
|
26
|
+
id: runtime.thinkingEntrySeq++,
|
|
27
|
+
text: "",
|
|
28
|
+
tokens: 0,
|
|
29
|
+
tokensExact: false,
|
|
30
|
+
active: true,
|
|
31
|
+
owner: null,
|
|
32
|
+
};
|
|
33
|
+
runtime.activeThinking = entry;
|
|
34
|
+
runtime.activeThinkingBlocks.clear();
|
|
35
|
+
runtime.unattachedThinking.push(entry);
|
|
36
|
+
return entry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function finalizeActiveThinking(): void {
|
|
40
|
+
const entry = runtime.activeThinking;
|
|
41
|
+
if (!entry) return;
|
|
42
|
+
entry.active = false;
|
|
43
|
+
entry.owner?.invalidate();
|
|
44
|
+
if (runtime.activeThinkingIndex !== null) runtime.sealedThinkingIndexes.add(runtime.activeThinkingIndex);
|
|
45
|
+
runtime.lastSealedThinkingEntry = entry;
|
|
46
|
+
runtime.activeThinking = null;
|
|
47
|
+
runtime.activeThinkingIndex = null;
|
|
48
|
+
runtime.activeThinkingBlocks.clear();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function refreshSealedThinkingTokens(reportedReasoning?: unknown): void {
|
|
52
|
+
const reported = Number(reportedReasoning);
|
|
53
|
+
if (Number.isFinite(reported) && reported > 0) runtime.lastReportedReasoningTokens = reported;
|
|
54
|
+
const entry = runtime.lastSealedThinkingEntry;
|
|
55
|
+
if (!entry || entry.tokensExact) return;
|
|
56
|
+
if (!entry.text.trim()) return; // no thinking row to correct
|
|
57
|
+
if (!(runtime.lastReportedReasoningTokens > 0)) return;
|
|
58
|
+
entry.tokens = runtime.lastReportedReasoningTokens;
|
|
59
|
+
entry.tokensExact = true;
|
|
60
|
+
entry.owner?.invalidate();
|
|
61
|
+
runtime.capturedTui?.requestRender?.();
|
|
62
|
+
}
|