pi-ui-extend 0.1.32 → 0.1.34
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 +1 -1
- package/dist/app/app.d.ts +2 -0
- package/dist/app/app.js +28 -0
- package/dist/app/commands/command-session-actions.js +29 -1
- package/dist/app/constants.d.ts +1 -1
- package/dist/app/constants.js +2 -2
- package/dist/app/icons.d.ts +4 -9
- package/dist/app/icons.js +12 -35
- package/dist/app/model/model-usage-status.d.ts +2 -1
- package/dist/app/model/model-usage-status.js +33 -25
- package/dist/app/rendering/conversation-entry-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-tool-renderer.d.ts +1 -0
- package/dist/app/rendering/conversation-tool-renderer.js +12 -18
- package/dist/app/rendering/conversation-viewport.d.ts +4 -0
- package/dist/app/rendering/conversation-viewport.js +144 -13
- package/dist/app/rendering/dcp-stats.js +42 -16
- package/dist/app/rendering/render-controller.js +4 -0
- package/dist/app/rendering/status-line-renderer.d.ts +8 -1
- package/dist/app/rendering/status-line-renderer.js +36 -1
- package/dist/app/rendering/tab-line-renderer.js +2 -2
- package/dist/app/rendering/tool-block-renderer.d.ts +1 -0
- package/dist/app/rendering/tool-block-renderer.js +37 -11
- package/dist/app/runtime.js +1 -1
- package/dist/app/screen/mouse-controller.d.ts +5 -1
- package/dist/app/screen/mouse-controller.js +16 -0
- package/dist/app/screen/scroll-controller.d.ts +20 -0
- package/dist/app/screen/scroll-controller.js +127 -10
- package/dist/app/session/lazy-session-manager.js +35 -5
- package/dist/app/session/pix-system-message.d.ts +1 -0
- package/dist/app/session/pix-system-message.js +14 -3
- package/dist/app/session/queued-message-controller.d.ts +11 -4
- package/dist/app/session/queued-message-controller.js +74 -59
- package/dist/app/session/queued-message-entries.d.ts +2 -1
- package/dist/app/session/queued-message-entries.js +12 -1
- package/dist/app/session/session-event-controller.d.ts +42 -1
- package/dist/app/session/session-event-controller.js +500 -31
- package/dist/app/session/session-history.js +23 -4
- package/dist/app/session/tabs-controller.d.ts +11 -1
- package/dist/app/session/tabs-controller.js +102 -21
- package/dist/app/types.d.ts +14 -1
- package/dist/bundled-extensions/question/contract.d.ts +25 -0
- package/dist/bundled-extensions/question/contract.js +94 -0
- package/dist/bundled-extensions/question/index.d.ts +7 -0
- package/dist/bundled-extensions/question/index.js +28 -0
- package/dist/bundled-extensions/question/render.d.ts +4 -0
- package/dist/bundled-extensions/question/render.js +27 -0
- package/dist/bundled-extensions/question/result.d.ts +6 -0
- package/dist/bundled-extensions/question/result.js +84 -0
- package/dist/bundled-extensions/question/tool-description.d.ts +7 -0
- package/dist/bundled-extensions/question/tool-description.js +11 -0
- package/dist/bundled-extensions/question/tui.d.ts +2 -0
- package/dist/bundled-extensions/question/tui.js +577 -0
- package/dist/bundled-extensions/question/types.d.ts +103 -0
- package/dist/bundled-extensions/question/types.js +1 -0
- package/dist/bundled-extensions/session-title/config.d.ts +17 -0
- package/dist/bundled-extensions/session-title/config.js +150 -0
- package/dist/bundled-extensions/session-title/index.d.ts +5 -0
- package/dist/bundled-extensions/session-title/index.js +384 -0
- package/dist/bundled-extensions/session-title/title-generation.d.ts +26 -0
- package/dist/bundled-extensions/session-title/title-generation.js +141 -0
- package/dist/bundled-extensions/terminal-bell/index.d.ts +14 -0
- package/dist/bundled-extensions/terminal-bell/index.js +491 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +2 -1
- package/dist/default-pix-config.js +2 -1
- package/dist/icon-theme.d.ts +7 -0
- package/dist/icon-theme.js +36 -0
- package/dist/schemas/pi-tools-suite-schema.d.ts +4 -0
- package/dist/schemas/pi-tools-suite-schema.js +5 -0
- package/dist/schemas/pix-schema.d.ts +1 -0
- package/dist/schemas/pix-schema.js +1 -0
- package/external/pi-tools-suite/README.md +7 -7
- package/external/pi-tools-suite/src/async-subagents/async-subagents.sample.jsonc +16 -16
- package/external/pi-tools-suite/src/async-subagents/core/state.ts +18 -4
- package/external/pi-tools-suite/src/async-subagents/core/types.ts +4 -0
- package/external/pi-tools-suite/src/async-subagents/tools/result.ts +14 -26
- package/external/pi-tools-suite/src/async-subagents/tools/subagents.ts +0 -1
- package/external/pi-tools-suite/src/dcp/config.ts +14 -14
- package/external/pi-tools-suite/src/dcp/index.ts +31 -43
- package/external/pi-tools-suite/src/dcp/state-persistence.ts +151 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +25 -18
- package/external/pi-tools-suite/src/tool-descriptions.ts +34 -54
- package/package.json +3 -2
- package/schemas/pi-tools-suite.json +14 -0
- package/schemas/pix.json +7 -0
- package/extensions/question/contract.ts +0 -100
- package/extensions/question/index.ts +0 -34
- package/extensions/question/render.ts +0 -28
- package/extensions/question/result.ts +0 -86
- package/extensions/question/tool-description.ts +0 -11
- package/extensions/question/tui.ts +0 -629
- package/extensions/question/types.ts +0 -123
- package/extensions/session-title/config.ts +0 -164
- package/extensions/session-title/index.ts +0 -502
- package/extensions/terminal-bell/index.ts +0 -345
|
@@ -1,21 +1,32 @@
|
|
|
1
1
|
import { createId } from "../id.js";
|
|
2
2
|
import { extractImageContents, renderContent, renderUserMessageContent, stringifyUnknown } from "../rendering/message-content.js";
|
|
3
3
|
import { customMessageEntry, loadSessionHistoryEntries, loadSessionHistoryEntriesAsync } from "./session-history.js";
|
|
4
|
-
import { sessionHistoryDisplayMessages,
|
|
4
|
+
import { sessionHistoryDisplayMessages, sessionHistoryDisplayMessagesFromEntries, sessionHistoryFullBranchEntries } from "./pix-system-message.js";
|
|
5
|
+
import { THINKING_TOOL_NAME } from "../constants.js";
|
|
5
6
|
import { isRecord } from "../guards.js";
|
|
6
7
|
const DCP_MESSAGE_REFERENCE_PREFIX = "[dcp-id]: # (m";
|
|
7
8
|
const DCP_BLOCK_REFERENCE_PREFIX = "[dcp-block-id]: # (b";
|
|
8
9
|
const MAX_HISTORY_WINDOW_ENTRIES = 360;
|
|
9
10
|
const HISTORY_WINDOW_TARGET_ENTRIES = 300;
|
|
11
|
+
const HISTORY_WINDOW_SHIFT_ENTRIES = 50;
|
|
10
12
|
export class AppSessionEventController {
|
|
11
13
|
host;
|
|
12
14
|
entryRenderVersions = new Map();
|
|
13
15
|
toolEntryIdsByCallId = new Map();
|
|
16
|
+
pendingToolCallIdsByContentIndex = new Map();
|
|
14
17
|
toolMutationPreparationsByCallId = new Map();
|
|
15
18
|
olderHistoryLoader;
|
|
16
19
|
currentUserEntryId;
|
|
17
20
|
currentAssistantEntryId;
|
|
21
|
+
currentAssistantTextBlockEntryId;
|
|
22
|
+
currentAssistantTextBlockStartLength;
|
|
23
|
+
currentAssistantTextBlockContentIndex;
|
|
24
|
+
assistantTextBlocksByContentIndex = new Map();
|
|
25
|
+
finalizedToolCallContentIndexes = new Set();
|
|
26
|
+
historyEntries = [];
|
|
27
|
+
historyWindowStart = 0;
|
|
18
28
|
currentThinkingEntryId;
|
|
29
|
+
assistantMessageClosed = false;
|
|
19
30
|
assistantTextBuffer = "";
|
|
20
31
|
constructor(host) {
|
|
21
32
|
this.host = host;
|
|
@@ -23,38 +34,67 @@ export class AppSessionEventController {
|
|
|
23
34
|
snapshotState() {
|
|
24
35
|
return {
|
|
25
36
|
toolEntryIdsByCallId: new Map(this.toolEntryIdsByCallId),
|
|
37
|
+
pendingToolCallIdsByContentIndex: new Map(this.pendingToolCallIdsByContentIndex),
|
|
26
38
|
toolMutationPreparationsByCallId: new Map(this.toolMutationPreparationsByCallId),
|
|
27
39
|
olderHistoryLoader: this.olderHistoryLoader,
|
|
28
40
|
currentUserEntryId: this.currentUserEntryId,
|
|
29
41
|
currentAssistantEntryId: this.currentAssistantEntryId,
|
|
42
|
+
currentAssistantTextBlockEntryId: this.currentAssistantTextBlockEntryId,
|
|
43
|
+
currentAssistantTextBlockStartLength: this.currentAssistantTextBlockStartLength,
|
|
44
|
+
currentAssistantTextBlockContentIndex: this.currentAssistantTextBlockContentIndex,
|
|
45
|
+
assistantTextBlocksByContentIndex: new Map(this.assistantTextBlocksByContentIndex),
|
|
30
46
|
currentThinkingEntryId: this.currentThinkingEntryId,
|
|
47
|
+
assistantMessageClosed: this.assistantMessageClosed,
|
|
31
48
|
assistantTextBuffer: this.assistantTextBuffer,
|
|
32
49
|
entryRenderVersions: new Map(this.entryRenderVersions),
|
|
50
|
+
historyEntries: [...this.historyEntries],
|
|
51
|
+
historyWindowStart: this.historyWindowStart,
|
|
33
52
|
};
|
|
34
53
|
}
|
|
35
54
|
restoreState(state) {
|
|
36
55
|
this.toolEntryIdsByCallId.clear();
|
|
37
56
|
for (const [key, value] of state.toolEntryIdsByCallId)
|
|
38
57
|
this.toolEntryIdsByCallId.set(key, value);
|
|
58
|
+
this.pendingToolCallIdsByContentIndex.clear();
|
|
59
|
+
for (const [key, value] of state.pendingToolCallIdsByContentIndex)
|
|
60
|
+
this.pendingToolCallIdsByContentIndex.set(key, value);
|
|
39
61
|
this.toolMutationPreparationsByCallId.clear();
|
|
40
62
|
for (const [key, value] of state.toolMutationPreparationsByCallId)
|
|
41
63
|
this.toolMutationPreparationsByCallId.set(key, value);
|
|
42
64
|
this.olderHistoryLoader = state.olderHistoryLoader;
|
|
43
65
|
this.currentUserEntryId = state.currentUserEntryId;
|
|
44
66
|
this.currentAssistantEntryId = state.currentAssistantEntryId;
|
|
67
|
+
this.currentAssistantTextBlockEntryId = state.currentAssistantTextBlockEntryId;
|
|
68
|
+
this.currentAssistantTextBlockStartLength = state.currentAssistantTextBlockStartLength;
|
|
69
|
+
this.currentAssistantTextBlockContentIndex = state.currentAssistantTextBlockContentIndex;
|
|
70
|
+
this.assistantTextBlocksByContentIndex.clear();
|
|
71
|
+
for (const [key, value] of state.assistantTextBlocksByContentIndex)
|
|
72
|
+
this.assistantTextBlocksByContentIndex.set(key, value);
|
|
45
73
|
this.currentThinkingEntryId = state.currentThinkingEntryId;
|
|
74
|
+
this.assistantMessageClosed = state.assistantMessageClosed;
|
|
46
75
|
this.assistantTextBuffer = state.assistantTextBuffer;
|
|
47
76
|
this.entryRenderVersions.clear();
|
|
48
77
|
for (const [key, value] of state.entryRenderVersions)
|
|
49
78
|
this.entryRenderVersions.set(key, value);
|
|
79
|
+
this.historyEntries = [...state.historyEntries];
|
|
80
|
+
this.historyWindowStart = state.historyWindowStart;
|
|
50
81
|
}
|
|
51
82
|
reset() {
|
|
52
83
|
this.toolEntryIdsByCallId.clear();
|
|
84
|
+
this.pendingToolCallIdsByContentIndex.clear();
|
|
53
85
|
this.toolMutationPreparationsByCallId.clear();
|
|
54
86
|
this.currentUserEntryId = undefined;
|
|
55
87
|
this.entryRenderVersions.clear();
|
|
88
|
+
this.historyEntries = [];
|
|
89
|
+
this.historyWindowStart = 0;
|
|
56
90
|
this.currentAssistantEntryId = undefined;
|
|
91
|
+
this.currentAssistantTextBlockEntryId = undefined;
|
|
92
|
+
this.currentAssistantTextBlockStartLength = undefined;
|
|
93
|
+
this.currentAssistantTextBlockContentIndex = undefined;
|
|
94
|
+
this.assistantTextBlocksByContentIndex.clear();
|
|
95
|
+
this.finalizedToolCallContentIndexes.clear();
|
|
57
96
|
this.currentThinkingEntryId = undefined;
|
|
97
|
+
this.assistantMessageClosed = false;
|
|
58
98
|
this.assistantTextBuffer = "";
|
|
59
99
|
this.olderHistoryLoader = undefined;
|
|
60
100
|
}
|
|
@@ -76,32 +116,53 @@ export class AppSessionEventController {
|
|
|
76
116
|
if (!runtime)
|
|
77
117
|
return !options.isCancelled();
|
|
78
118
|
this.olderHistoryLoader = undefined;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
119
|
+
this.historyEntries = [];
|
|
120
|
+
this.historyWindowStart = 0;
|
|
121
|
+
const branchEntries = await sessionHistoryFullBranchEntries(runtime.session);
|
|
122
|
+
if (options.isCancelled())
|
|
123
|
+
return false;
|
|
124
|
+
const historyEntries = [];
|
|
125
|
+
const loaded = await loadSessionHistoryEntriesAsync({
|
|
126
|
+
messages: sessionHistoryDisplayMessagesFromEntries(branchEntries),
|
|
127
|
+
addEntry: (entry) => historyEntries.push(entry),
|
|
128
|
+
prependEntries: (entries) => historyEntries.unshift(...entries),
|
|
84
129
|
setToolEntryId: (toolCallId, entryId) => this.toolEntryIdsByCallId.set(toolCallId, entryId),
|
|
85
130
|
toolDefaultExpanded: (toolName) => this.host.toolDefaultExpanded(toolName),
|
|
86
131
|
observeSubagentsToolResult: (toolName, details, options) => this.host.observeSubagentsToolResult(toolName, details, options),
|
|
87
132
|
observeTodoToolResult: (toolName, details, isError) => this.host.observeTodoToolResult(toolName, details, isError),
|
|
88
133
|
isCancelled: options.isCancelled,
|
|
89
|
-
render:
|
|
90
|
-
lazyOlderHistory:
|
|
91
|
-
onOlderLoaderReady: (loader) => {
|
|
92
|
-
this.olderHistoryLoader = loader;
|
|
93
|
-
},
|
|
134
|
+
render: () => { },
|
|
135
|
+
lazyOlderHistory: false,
|
|
94
136
|
});
|
|
137
|
+
if (!loaded)
|
|
138
|
+
return false;
|
|
139
|
+
this.historyEntries = historyEntries;
|
|
140
|
+
this.setHistoryWindowStart(this.maxHistoryWindowStart());
|
|
141
|
+
options.render();
|
|
142
|
+
return !options.isCancelled();
|
|
95
143
|
}
|
|
96
144
|
hasOlderSessionHistory() {
|
|
145
|
+
if (this.historyEntries.length > 0)
|
|
146
|
+
return this.historyWindowStart > 0;
|
|
97
147
|
return this.olderHistoryLoader?.hasOlder() === true;
|
|
98
148
|
}
|
|
99
149
|
isLoadingOlderSessionHistory() {
|
|
100
150
|
return this.olderHistoryLoader?.isLoading() === true;
|
|
101
151
|
}
|
|
102
152
|
async loadOlderSessionHistory(options = {}) {
|
|
153
|
+
if (this.historyEntries.length > 0)
|
|
154
|
+
return this.shiftHistoryWindow(-HISTORY_WINDOW_SHIFT_ENTRIES, options);
|
|
103
155
|
return this.olderHistoryLoader?.loadOlder(options) ?? false;
|
|
104
156
|
}
|
|
157
|
+
hasNewerSessionHistory() {
|
|
158
|
+
return this.historyEntries.length > 0 && this.historyWindowStart < this.maxHistoryWindowStart();
|
|
159
|
+
}
|
|
160
|
+
isLoadingNewerSessionHistory() {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
async loadNewerSessionHistory(options = {}) {
|
|
164
|
+
return this.shiftHistoryWindow(HISTORY_WINDOW_SHIFT_ENTRIES, options);
|
|
165
|
+
}
|
|
105
166
|
handleSessionEvent(event) {
|
|
106
167
|
switch (event.type) {
|
|
107
168
|
case "session_info_changed":
|
|
@@ -114,6 +175,7 @@ export class AppSessionEventController {
|
|
|
114
175
|
this.handleMessageEnd(event.message);
|
|
115
176
|
break;
|
|
116
177
|
case "agent_start":
|
|
178
|
+
this.assistantMessageClosed = false;
|
|
117
179
|
this.host.setSessionActivity("running");
|
|
118
180
|
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
119
181
|
break;
|
|
@@ -123,19 +185,22 @@ export class AppSessionEventController {
|
|
|
123
185
|
case "agent_end":
|
|
124
186
|
this.finishCurrentThinkingEntry();
|
|
125
187
|
this.clearCurrentAssistantState();
|
|
188
|
+
this.finalizeAbandonedToolEntries();
|
|
126
189
|
this.currentUserEntryId = undefined;
|
|
127
190
|
this.host.setSessionActivity("idle");
|
|
128
191
|
this.host.setSessionStatus(this.host.runtime()?.session);
|
|
192
|
+
this.host.flushAutoUserMessages();
|
|
129
193
|
break;
|
|
130
194
|
case "queue_update":
|
|
131
195
|
this.host.updateQueuedMessageStatus();
|
|
132
196
|
break;
|
|
133
197
|
case "message_update":
|
|
134
|
-
this.handleMessageUpdate(event
|
|
198
|
+
this.handleMessageUpdate(event);
|
|
135
199
|
break;
|
|
136
200
|
case "tool_execution_start":
|
|
137
201
|
this.finishCurrentThinkingEntry();
|
|
138
202
|
this.flushAssistantTextBuffer(true);
|
|
203
|
+
this.finishCurrentAssistantTextBlock();
|
|
139
204
|
this.currentAssistantEntryId = undefined;
|
|
140
205
|
this.host.setSessionActivity("running");
|
|
141
206
|
this.prepareToolWorkspaceMutation(event.toolCallId, event.toolName, event.args);
|
|
@@ -181,6 +246,7 @@ export class AppSessionEventController {
|
|
|
181
246
|
case "compaction_end": {
|
|
182
247
|
this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
|
|
183
248
|
this.host.restoreSessionStatus();
|
|
249
|
+
this.host.flushAutoUserMessages();
|
|
184
250
|
const message = event.result
|
|
185
251
|
? `Compacted ${event.result.tokensBefore} tokens`
|
|
186
252
|
: event.aborted
|
|
@@ -196,6 +262,7 @@ export class AppSessionEventController {
|
|
|
196
262
|
case "auto_retry_end":
|
|
197
263
|
this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
|
|
198
264
|
this.host.restoreSessionStatus();
|
|
265
|
+
this.host.flushAutoUserMessages();
|
|
199
266
|
this.host.showToast(event.success ? "Retry succeeded" : `Retry failed: ${event.finalError}`, event.success ? "success" : "error");
|
|
200
267
|
break;
|
|
201
268
|
default:
|
|
@@ -209,7 +276,7 @@ export class AppSessionEventController {
|
|
|
209
276
|
this.addEntry(entry);
|
|
210
277
|
}
|
|
211
278
|
findEntry(id) {
|
|
212
|
-
return this.host.entries.find((entry) => entry.id === id);
|
|
279
|
+
return this.host.entries.find((entry) => entry.id === id) ?? this.historyEntries.find((entry) => entry.id === id);
|
|
213
280
|
}
|
|
214
281
|
findUserEntry(id) {
|
|
215
282
|
const entry = this.findEntry(id);
|
|
@@ -220,28 +287,71 @@ export class AppSessionEventController {
|
|
|
220
287
|
this.host.conversationViewport().deleteEntry(entry.id);
|
|
221
288
|
}
|
|
222
289
|
addEntry(entry) {
|
|
290
|
+
if (this.historyEntries.length > 0) {
|
|
291
|
+
const wasAtBottom = !this.hasNewerSessionHistory();
|
|
292
|
+
this.historyEntries.push(entry);
|
|
293
|
+
this.registerEntry(entry);
|
|
294
|
+
if (wasAtBottom)
|
|
295
|
+
this.setHistoryWindowStart(this.maxHistoryWindowStart());
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
223
298
|
this.host.entries.push(entry);
|
|
224
|
-
this.
|
|
225
|
-
this.host.conversationViewport().deleteEntry(entry.id);
|
|
299
|
+
this.registerEntry(entry);
|
|
226
300
|
this.pruneHistoryWindow("top");
|
|
227
301
|
}
|
|
228
302
|
prependEntries(entries) {
|
|
229
303
|
this.host.entries.unshift(...entries);
|
|
230
|
-
for (const entry of entries)
|
|
231
|
-
this.
|
|
232
|
-
this.host.conversationViewport().deleteEntry(entry.id);
|
|
233
|
-
}
|
|
304
|
+
for (const entry of entries)
|
|
305
|
+
this.registerEntry(entry);
|
|
234
306
|
this.pruneHistoryWindow("bottom");
|
|
235
307
|
}
|
|
308
|
+
shiftHistoryWindow(delta, options = {}) {
|
|
309
|
+
if (this.historyEntries.length === 0)
|
|
310
|
+
return false;
|
|
311
|
+
const previousStart = this.historyWindowStart;
|
|
312
|
+
const nextStart = Math.max(0, Math.min(this.maxHistoryWindowStart(), previousStart + delta));
|
|
313
|
+
if (nextStart === previousStart)
|
|
314
|
+
return true;
|
|
315
|
+
if (nextStart < previousStart)
|
|
316
|
+
options.onPrependedEntries?.(this.historyEntries.slice(nextStart, previousStart));
|
|
317
|
+
this.setHistoryWindowStart(nextStart);
|
|
318
|
+
if (options.render !== false)
|
|
319
|
+
this.host.render();
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
setHistoryWindowStart(start) {
|
|
323
|
+
const nextStart = Math.max(0, Math.min(this.maxHistoryWindowStart(), start));
|
|
324
|
+
this.historyWindowStart = nextStart;
|
|
325
|
+
const nextEntries = this.historyEntries.slice(nextStart, nextStart + this.historyWindowSize());
|
|
326
|
+
const nextEntryIds = new Set(nextEntries.map((entry) => entry.id));
|
|
327
|
+
for (const entry of this.host.entries) {
|
|
328
|
+
if (!nextEntryIds.has(entry.id))
|
|
329
|
+
this.host.conversationViewport().deleteEntry(entry.id);
|
|
330
|
+
}
|
|
331
|
+
this.host.entries.splice(0, this.host.entries.length, ...nextEntries);
|
|
332
|
+
for (const entry of nextEntries)
|
|
333
|
+
this.registerEntry(entry);
|
|
334
|
+
}
|
|
335
|
+
historyWindowSize() {
|
|
336
|
+
return Math.min(HISTORY_WINDOW_TARGET_ENTRIES, this.historyEntries.length);
|
|
337
|
+
}
|
|
338
|
+
maxHistoryWindowStart() {
|
|
339
|
+
return Math.max(0, this.historyEntries.length - this.historyWindowSize());
|
|
340
|
+
}
|
|
341
|
+
registerEntry(entry) {
|
|
342
|
+
this.entryRenderVersions.set(entry.id, this.entryRenderVersions.get(entry.id) ?? 1);
|
|
343
|
+
if (entry.kind === "tool")
|
|
344
|
+
this.toolEntryIdsByCallId.set(entry.toolCallId, entry.id);
|
|
345
|
+
this.host.conversationViewport().deleteEntry(entry.id);
|
|
346
|
+
}
|
|
236
347
|
pruneHistoryWindow(edge) {
|
|
237
348
|
const removeCount = this.host.entries.length - MAX_HISTORY_WINDOW_ENTRIES;
|
|
238
349
|
if (removeCount <= 0)
|
|
239
350
|
return;
|
|
240
351
|
const targetRemoveCount = Math.max(removeCount, this.host.entries.length - HISTORY_WINDOW_TARGET_ENTRIES);
|
|
241
|
-
const
|
|
242
|
-
? this.host.entries.slice(0, targetRemoveCount).map((entry) => entry.id)
|
|
243
|
-
:
|
|
244
|
-
const removedLineCount = this.measuredLineCountForEntries(removedEntryIds);
|
|
352
|
+
const removedLineCount = edge === "top"
|
|
353
|
+
? this.measuredLineCountForEntries(this.host.entries.slice(0, targetRemoveCount).map((entry) => entry.id))
|
|
354
|
+
: 0;
|
|
245
355
|
const removed = edge === "top"
|
|
246
356
|
? this.host.entries.splice(0, targetRemoveCount)
|
|
247
357
|
: this.host.entries.splice(Math.max(0, this.host.entries.length - targetRemoveCount), targetRemoveCount);
|
|
@@ -266,18 +376,50 @@ export class AppSessionEventController {
|
|
|
266
376
|
if (entryId === entry.id)
|
|
267
377
|
this.toolEntryIdsByCallId.delete(toolCallId);
|
|
268
378
|
}
|
|
379
|
+
for (const [contentIndex, toolCallId] of this.pendingToolCallIdsByContentIndex) {
|
|
380
|
+
if (this.toolEntryIdsByCallId.get(toolCallId) === undefined)
|
|
381
|
+
this.pendingToolCallIdsByContentIndex.delete(contentIndex);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
removeToolEntryOrphan(entryId) {
|
|
385
|
+
const index = this.host.entries.findIndex((entry) => entry.id === entryId);
|
|
386
|
+
if (index === -1)
|
|
387
|
+
return;
|
|
388
|
+
const removed = this.host.entries.splice(index, 1)[0];
|
|
389
|
+
if (removed === undefined)
|
|
390
|
+
return;
|
|
391
|
+
this.forgetEntry(removed);
|
|
392
|
+
this.host.scheduleRender();
|
|
269
393
|
}
|
|
270
394
|
addSessionAbortedEntry() {
|
|
271
395
|
this.finishCurrentThinkingEntry();
|
|
272
396
|
this.clearCurrentAssistantState();
|
|
397
|
+
this.finalizeAbandonedToolEntries();
|
|
273
398
|
this.addEntry({ id: createId("session-aborted"), kind: "session-aborted", text: "Session aborted." });
|
|
274
399
|
}
|
|
400
|
+
// D.16: when a turn aborts (agent_end, error, or session abort) without the
|
|
401
|
+
// SDK emitting tool_execution_end for a tool that already started executing,
|
|
402
|
+
// mark any running tool entries as done so they don't render a perpetual
|
|
403
|
+
// spinner. The output is left as whatever partial was last observed.
|
|
404
|
+
finalizeAbandonedToolEntries() {
|
|
405
|
+
let touched = false;
|
|
406
|
+
for (const entry of this.host.entries) {
|
|
407
|
+
if (entry.kind === "tool" && entry.status === "running") {
|
|
408
|
+
entry.status = "done";
|
|
409
|
+
this.touchEntry(entry);
|
|
410
|
+
touched = true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (touched)
|
|
414
|
+
this.host.scheduleRender();
|
|
415
|
+
}
|
|
275
416
|
handleMessageStart(message) {
|
|
276
417
|
if (isRecord(message) && message.role === "custom") {
|
|
277
418
|
this.addCustomMessageEntry(message);
|
|
278
419
|
return;
|
|
279
420
|
}
|
|
280
421
|
if (isRecord(message) && message.role === "user") {
|
|
422
|
+
this.assistantMessageClosed = false;
|
|
281
423
|
const text = renderUserMessageContent(message.content);
|
|
282
424
|
if (!text)
|
|
283
425
|
return;
|
|
@@ -293,17 +435,25 @@ export class AppSessionEventController {
|
|
|
293
435
|
this.currentUserEntryId = entryId;
|
|
294
436
|
return;
|
|
295
437
|
}
|
|
296
|
-
if (isRecord(message) && message.role === "assistant")
|
|
438
|
+
if (isRecord(message) && message.role === "assistant") {
|
|
439
|
+
this.assistantMessageClosed = false;
|
|
297
440
|
this.clearCurrentAssistantState();
|
|
441
|
+
}
|
|
298
442
|
}
|
|
299
443
|
handleMessageEnd(message) {
|
|
300
444
|
if (isRecord(message) && message.role === "user") {
|
|
301
445
|
this.host.scheduleUserSessionEntryMetadataSync();
|
|
302
446
|
}
|
|
447
|
+
if (isRecord(message) && message.role === "toolResult") {
|
|
448
|
+
this.finalizeToolResultMessage(message);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
303
451
|
if (isRecord(message) && message.role === "assistant") {
|
|
452
|
+
this.renderAssistantToolCallsFromMessage(message);
|
|
304
453
|
this.finishCurrentThinkingEntry();
|
|
305
454
|
this.flushAssistantTextBuffer(true);
|
|
306
455
|
this.clearCurrentAssistantState();
|
|
456
|
+
this.assistantMessageClosed = true;
|
|
307
457
|
}
|
|
308
458
|
}
|
|
309
459
|
prepareToolWorkspaceMutation(toolCallId, toolName, args) {
|
|
@@ -317,6 +467,29 @@ export class AppSessionEventController {
|
|
|
317
467
|
...(preparation === undefined ? {} : { preparation }),
|
|
318
468
|
});
|
|
319
469
|
}
|
|
470
|
+
finalizeToolResultMessage(message) {
|
|
471
|
+
const toolCallId = typeof message.toolCallId === "string" ? message.toolCallId : undefined;
|
|
472
|
+
const toolName = typeof message.toolName === "string" ? message.toolName : undefined;
|
|
473
|
+
const content = Array.isArray(message.content) ? message.content : [];
|
|
474
|
+
const details = message.details;
|
|
475
|
+
const isError = Boolean(message.isError);
|
|
476
|
+
if (toolCallId && toolName) {
|
|
477
|
+
this.recordToolWorkspaceMutation(toolCallId, toolName, details, isError);
|
|
478
|
+
if (this.currentUserEntryId)
|
|
479
|
+
this.host.scheduleUserSessionEntryMetadataSync();
|
|
480
|
+
this.host.observeSubagentsToolResult(toolName, details);
|
|
481
|
+
this.host.observeTodoToolResult(toolName, details, isError);
|
|
482
|
+
this.upsertToolEntry(toolCallId, {
|
|
483
|
+
toolName,
|
|
484
|
+
output: renderContent(content),
|
|
485
|
+
images: extractImageContents(content),
|
|
486
|
+
details,
|
|
487
|
+
isError,
|
|
488
|
+
status: "done",
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
|
|
492
|
+
}
|
|
320
493
|
recordToolWorkspaceMutation(toolCallId, toolName, details, isError) {
|
|
321
494
|
const prepared = this.toolMutationPreparationsByCallId.get(toolCallId);
|
|
322
495
|
if (!prepared)
|
|
@@ -333,21 +506,68 @@ export class AppSessionEventController {
|
|
|
333
506
|
return;
|
|
334
507
|
this.host.recordWorkspaceMutationForUserEntry(prepared.userEntryId, mutation);
|
|
335
508
|
}
|
|
336
|
-
handleMessageUpdate(
|
|
509
|
+
handleMessageUpdate(event) {
|
|
510
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
511
|
+
if (this.assistantMessageClosed && assistantEvent.type !== "done")
|
|
512
|
+
return;
|
|
513
|
+
this.assistantMessageClosed = false;
|
|
337
514
|
switch (assistantEvent.type) {
|
|
515
|
+
case "text_start":
|
|
516
|
+
this.finishCurrentThinkingEntry();
|
|
517
|
+
this.flushAssistantTextBuffer(true);
|
|
518
|
+
this.finishCurrentAssistantTextBlock();
|
|
519
|
+
this.currentAssistantTextBlockEntryId = undefined;
|
|
520
|
+
this.currentAssistantTextBlockStartLength = undefined;
|
|
521
|
+
this.currentAssistantTextBlockContentIndex = assistantEvent.contentIndex;
|
|
522
|
+
break;
|
|
338
523
|
case "text_delta":
|
|
339
524
|
this.finishCurrentThinkingEntry();
|
|
340
525
|
this.host.setSessionActivity("running");
|
|
341
|
-
|
|
526
|
+
{
|
|
527
|
+
const snapshotText = assistantTextSnapshotForContentIndex(event.message, assistantEvent.partial, assistantEvent.contentIndex);
|
|
528
|
+
if (snapshotText === undefined)
|
|
529
|
+
this.appendAssistantText(assistantEvent.delta, assistantEvent.contentIndex);
|
|
530
|
+
else
|
|
531
|
+
this.reconcileAssistantTextBlock(snapshotText, assistantEvent.contentIndex, { keepOpen: true });
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
case "text_end":
|
|
535
|
+
this.finishCurrentThinkingEntry();
|
|
536
|
+
this.host.setSessionActivity("running");
|
|
537
|
+
this.reconcileAssistantTextBlock(assistantEvent.content, assistantEvent.contentIndex);
|
|
342
538
|
break;
|
|
343
539
|
case "thinking_delta":
|
|
540
|
+
if (assistantEvent.delta.length === 0)
|
|
541
|
+
return;
|
|
542
|
+
if (this.currentThinkingEntryId === undefined && this.hasStartedCurrentAssistantText())
|
|
543
|
+
return;
|
|
344
544
|
this.host.setSessionActivity("thinking");
|
|
345
545
|
this.appendThinkingText(assistantEvent.delta);
|
|
346
546
|
break;
|
|
547
|
+
case "thinking_end":
|
|
548
|
+
if (this.currentThinkingEntryId === undefined && this.hasStartedCurrentAssistantText())
|
|
549
|
+
return;
|
|
550
|
+
if (assistantEvent.content.length === 0) {
|
|
551
|
+
this.finishCurrentThinkingEntry();
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
this.host.setSessionActivity("thinking");
|
|
555
|
+
this.reconcileThinkingText(assistantEvent.content);
|
|
556
|
+
this.finishCurrentThinkingEntry();
|
|
557
|
+
break;
|
|
558
|
+
case "toolcall_start":
|
|
559
|
+
case "toolcall_delta":
|
|
560
|
+
this.handleToolCallStreamUpdate(assistantEvent.contentIndex, assistantEvent.partial);
|
|
561
|
+
break;
|
|
562
|
+
case "toolcall_end":
|
|
563
|
+
this.handleToolCallStreamUpdate(assistantEvent.contentIndex, assistantEvent.partial, assistantEvent.toolCall);
|
|
564
|
+
break;
|
|
347
565
|
case "done":
|
|
566
|
+
this.renderAssistantToolCallsFromMessage(assistantEvent.message);
|
|
348
567
|
this.finishCurrentThinkingEntry();
|
|
349
568
|
this.flushAssistantTextBuffer(true);
|
|
350
569
|
this.clearCurrentAssistantState();
|
|
570
|
+
this.assistantMessageClosed = true;
|
|
351
571
|
this.host.setSessionActivity(this.host.runtime()?.session.isStreaming ? "running" : "idle");
|
|
352
572
|
break;
|
|
353
573
|
case "error":
|
|
@@ -360,8 +580,33 @@ export class AppSessionEventController {
|
|
|
360
580
|
break;
|
|
361
581
|
}
|
|
362
582
|
}
|
|
363
|
-
|
|
364
|
-
|
|
583
|
+
handleToolCallStreamUpdate(contentIndex, partial, finalToolCall) {
|
|
584
|
+
// B.6: once a tool call block has been finalised by toolcall_end, a later
|
|
585
|
+
// toolcall_delta for the same contentIndex is a stale re-stream of the
|
|
586
|
+
// partial args. Applying it would clobber the final, complete args with
|
|
587
|
+
// an incomplete version. Ignore it.
|
|
588
|
+
if (finalToolCall === undefined && contentIndex !== undefined && this.finalizedToolCallContentIndexes.has(contentIndex)) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
this.finishCurrentThinkingEntry();
|
|
592
|
+
this.flushAssistantTextBuffer(true);
|
|
593
|
+
this.finishCurrentAssistantTextBlock();
|
|
594
|
+
this.currentAssistantEntryId = undefined;
|
|
595
|
+
this.currentAssistantTextBlockEntryId = undefined;
|
|
596
|
+
this.currentAssistantTextBlockStartLength = undefined;
|
|
597
|
+
this.currentAssistantTextBlockContentIndex = undefined;
|
|
598
|
+
this.host.setSessionActivity("running");
|
|
599
|
+
this.upsertPendingToolCall(finalToolCall ?? partialToolCallAt(partial, contentIndex), contentIndex);
|
|
600
|
+
if (finalToolCall !== undefined && contentIndex !== undefined)
|
|
601
|
+
this.finalizedToolCallContentIndexes.add(contentIndex);
|
|
602
|
+
}
|
|
603
|
+
appendAssistantText(delta, contentIndex) {
|
|
604
|
+
if (contentIndex !== undefined)
|
|
605
|
+
this.currentAssistantTextBlockContentIndex = contentIndex;
|
|
606
|
+
// C.11: providers may emit Windows-style CRLF (\r\n) line endings. The
|
|
607
|
+
// drain logic splits on "\n", which would leave a dangling "\r" at the
|
|
608
|
+
// end of each line and corrupt width/layout calculations. Normalise to LF.
|
|
609
|
+
this.assistantTextBuffer += delta.includes("\r") ? delta.replace(/\r\n?/gu, "\n") : delta;
|
|
365
610
|
this.flushAssistantTextBuffer(false);
|
|
366
611
|
}
|
|
367
612
|
flushAssistantTextBuffer(final) {
|
|
@@ -374,9 +619,97 @@ export class AppSessionEventController {
|
|
|
374
619
|
this.addEntry(entry);
|
|
375
620
|
this.currentAssistantEntryId = entry.id;
|
|
376
621
|
}
|
|
622
|
+
this.ensureAssistantTextBlockStarted(entry);
|
|
377
623
|
entry.text += visibleText;
|
|
378
624
|
this.touchEntry(entry);
|
|
379
625
|
}
|
|
626
|
+
reconcileAssistantTextBlock(content, contentIndex, options = {}) {
|
|
627
|
+
this.flushAssistantTextBuffer(true);
|
|
628
|
+
const hasVisibleTextBeforeBlock = this.hasVisibleTextBeforeCurrentAssistantBlock();
|
|
629
|
+
// C.11: normalise CRLF in the final block content (see appendAssistantText).
|
|
630
|
+
const normalisedContent = content.includes("\r") ? content.replace(/\r\n?/gu, "\n") : content;
|
|
631
|
+
const visibleText = assistantStreamVisibleTextForCompleteBlock(normalisedContent, hasVisibleTextBeforeBlock);
|
|
632
|
+
// B.5: a late text_end for a block that was already finished (it is not the
|
|
633
|
+
// currently-streaming block and is already recorded) must not clobber the
|
|
634
|
+
// current block or duplicate the early block's text.
|
|
635
|
+
if (contentIndex !== undefined
|
|
636
|
+
&& this.currentAssistantTextBlockContentIndex !== undefined
|
|
637
|
+
&& contentIndex !== this.currentAssistantTextBlockContentIndex
|
|
638
|
+
&& this.assistantTextBlocksByContentIndex.has(contentIndex))
|
|
639
|
+
return;
|
|
640
|
+
if (this.currentAssistantTextBlockEntryId === undefined
|
|
641
|
+
&& contentIndex !== undefined
|
|
642
|
+
&& this.assistantTextBlocksByContentIndex.get(contentIndex) === visibleText)
|
|
643
|
+
return;
|
|
644
|
+
// A.1: a text_end carrying empty content (provider content-filtering /
|
|
645
|
+
// truncation quirk) must not wipe assistant text already rendered to the
|
|
646
|
+
// user. Only finalize the block state, leaving any committed text intact.
|
|
647
|
+
if (!visibleText)
|
|
648
|
+
return;
|
|
649
|
+
let entry = this.currentAssistantTextBlockEntryId ? this.findEntry(this.currentAssistantTextBlockEntryId) : undefined;
|
|
650
|
+
if (!entry || entry.kind !== "assistant") {
|
|
651
|
+
entry = this.currentAssistantEntryId ? this.findEntry(this.currentAssistantEntryId) : undefined;
|
|
652
|
+
}
|
|
653
|
+
if (!entry || entry.kind !== "assistant") {
|
|
654
|
+
if (!visibleText)
|
|
655
|
+
return;
|
|
656
|
+
entry = { id: createId("assistant"), kind: "assistant", text: "" };
|
|
657
|
+
this.addEntry(entry);
|
|
658
|
+
this.currentAssistantEntryId = entry.id;
|
|
659
|
+
}
|
|
660
|
+
const startLength = this.currentAssistantTextBlockEntryId === entry.id
|
|
661
|
+
? Math.min(this.currentAssistantTextBlockStartLength ?? entry.text.length, entry.text.length)
|
|
662
|
+
: entry.text.length;
|
|
663
|
+
const currentBlockText = entry.text.slice(startLength);
|
|
664
|
+
// C.10: when the final block content is a strict prefix of what was
|
|
665
|
+
// already streamed to the user (provider truncation / retroactive
|
|
666
|
+
// content filtering), preserve the longer streamed text rather than
|
|
667
|
+
// silently retracting already-rendered content. Same principle as A.1.
|
|
668
|
+
const preserveStreamedText = visibleText.length < currentBlockText.length
|
|
669
|
+
&& currentBlockText.startsWith(visibleText);
|
|
670
|
+
if (!preserveStreamedText && currentBlockText !== visibleText) {
|
|
671
|
+
entry.text = `${entry.text.slice(0, startLength)}${visibleText}`;
|
|
672
|
+
this.touchEntry(entry);
|
|
673
|
+
}
|
|
674
|
+
this.currentAssistantEntryId = entry.id;
|
|
675
|
+
if (contentIndex !== undefined)
|
|
676
|
+
this.assistantTextBlocksByContentIndex.set(contentIndex, visibleText);
|
|
677
|
+
if (options.keepOpen) {
|
|
678
|
+
this.currentAssistantTextBlockEntryId = entry.id;
|
|
679
|
+
this.currentAssistantTextBlockStartLength = startLength;
|
|
680
|
+
this.currentAssistantTextBlockContentIndex = contentIndex;
|
|
681
|
+
}
|
|
682
|
+
else {
|
|
683
|
+
this.currentAssistantTextBlockEntryId = undefined;
|
|
684
|
+
this.currentAssistantTextBlockStartLength = undefined;
|
|
685
|
+
this.currentAssistantTextBlockContentIndex = undefined;
|
|
686
|
+
}
|
|
687
|
+
this.assistantTextBuffer = "";
|
|
688
|
+
}
|
|
689
|
+
ensureAssistantTextBlockStarted(entry) {
|
|
690
|
+
if (this.currentAssistantTextBlockEntryId === entry.id && this.currentAssistantTextBlockStartLength !== undefined)
|
|
691
|
+
return;
|
|
692
|
+
this.currentAssistantTextBlockEntryId = entry.id;
|
|
693
|
+
this.currentAssistantTextBlockStartLength = entry.text.length;
|
|
694
|
+
}
|
|
695
|
+
finishCurrentAssistantTextBlock() {
|
|
696
|
+
if (this.currentAssistantTextBlockContentIndex !== undefined && this.currentAssistantTextBlockEntryId !== undefined) {
|
|
697
|
+
const entry = this.findEntry(this.currentAssistantTextBlockEntryId);
|
|
698
|
+
if (entry?.kind === "assistant") {
|
|
699
|
+
const startLength = Math.min(this.currentAssistantTextBlockStartLength ?? entry.text.length, entry.text.length);
|
|
700
|
+
this.assistantTextBlocksByContentIndex.set(this.currentAssistantTextBlockContentIndex, entry.text.slice(startLength));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
this.currentAssistantTextBlockEntryId = undefined;
|
|
704
|
+
this.currentAssistantTextBlockStartLength = undefined;
|
|
705
|
+
this.currentAssistantTextBlockContentIndex = undefined;
|
|
706
|
+
}
|
|
707
|
+
hasVisibleTextBeforeCurrentAssistantBlock() {
|
|
708
|
+
const entry = this.currentAssistantTextBlockEntryId ? this.findEntry(this.currentAssistantTextBlockEntryId) : undefined;
|
|
709
|
+
if (entry?.kind !== "assistant")
|
|
710
|
+
return this.hasVisibleAssistantText("");
|
|
711
|
+
return (this.currentAssistantTextBlockStartLength ?? 0) > 0;
|
|
712
|
+
}
|
|
380
713
|
drainAssistantTextBuffer(final) {
|
|
381
714
|
let visibleText = "";
|
|
382
715
|
for (;;) {
|
|
@@ -409,13 +742,34 @@ export class AppSessionEventController {
|
|
|
409
742
|
const entry = this.currentAssistantEntryId ? this.findEntry(this.currentAssistantEntryId) : undefined;
|
|
410
743
|
return entry?.kind === "assistant" && entry.text.length > 0;
|
|
411
744
|
}
|
|
745
|
+
hasStartedCurrentAssistantText() {
|
|
746
|
+
if (this.assistantTextBuffer.length > 0)
|
|
747
|
+
return true;
|
|
748
|
+
const entry = this.currentAssistantEntryId ? this.findEntry(this.currentAssistantEntryId) : undefined;
|
|
749
|
+
return entry?.kind === "assistant" && entry.text.length > 0;
|
|
750
|
+
}
|
|
412
751
|
appendThinkingText(delta) {
|
|
413
|
-
let entry = this.currentThinkingEntryId
|
|
752
|
+
let entry = this.currentThinkingEntryId
|
|
753
|
+
? this.findEntry(this.currentThinkingEntryId)
|
|
754
|
+
: undefined;
|
|
414
755
|
if (!entry || entry.kind !== "thinking") {
|
|
415
|
-
|
|
756
|
+
const level = this.currentThinkingLevel();
|
|
757
|
+
entry = {
|
|
758
|
+
id: createId("thinking"),
|
|
759
|
+
kind: "thinking",
|
|
760
|
+
text: "",
|
|
761
|
+
expanded: this.host.toolDefaultExpanded(THINKING_TOOL_NAME),
|
|
762
|
+
...(level === undefined ? {} : { level }),
|
|
763
|
+
status: "running",
|
|
764
|
+
};
|
|
416
765
|
this.addEntry(entry);
|
|
417
766
|
this.currentThinkingEntryId = entry.id;
|
|
418
767
|
}
|
|
768
|
+
const level = this.currentThinkingLevel();
|
|
769
|
+
if (level === undefined)
|
|
770
|
+
delete entry.level;
|
|
771
|
+
else
|
|
772
|
+
entry.level = level;
|
|
419
773
|
entry.status = "running";
|
|
420
774
|
entry.text += delta;
|
|
421
775
|
this.touchEntry(entry);
|
|
@@ -428,13 +782,86 @@ export class AppSessionEventController {
|
|
|
428
782
|
}
|
|
429
783
|
this.currentThinkingEntryId = undefined;
|
|
430
784
|
}
|
|
785
|
+
reconcileThinkingText(content) {
|
|
786
|
+
let entry = this.currentThinkingEntryId
|
|
787
|
+
? this.findEntry(this.currentThinkingEntryId)
|
|
788
|
+
: undefined;
|
|
789
|
+
if (!entry || entry.kind !== "thinking") {
|
|
790
|
+
const level = this.currentThinkingLevel();
|
|
791
|
+
entry = {
|
|
792
|
+
id: createId("thinking"),
|
|
793
|
+
kind: "thinking",
|
|
794
|
+
text: "",
|
|
795
|
+
expanded: this.host.toolDefaultExpanded(THINKING_TOOL_NAME),
|
|
796
|
+
...(level === undefined ? {} : { level }),
|
|
797
|
+
status: "running",
|
|
798
|
+
};
|
|
799
|
+
this.addEntry(entry);
|
|
800
|
+
this.currentThinkingEntryId = entry.id;
|
|
801
|
+
}
|
|
802
|
+
const level = this.currentThinkingLevel();
|
|
803
|
+
if (entry.text !== content || entry.status !== "running" || entry.level !== level) {
|
|
804
|
+
entry.text = content;
|
|
805
|
+
if (level === undefined)
|
|
806
|
+
delete entry.level;
|
|
807
|
+
else
|
|
808
|
+
entry.level = level;
|
|
809
|
+
entry.status = "running";
|
|
810
|
+
this.touchEntry(entry);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
currentThinkingLevel() {
|
|
814
|
+
return this.host.runtime()?.session.thinkingLevel;
|
|
815
|
+
}
|
|
816
|
+
renderAssistantToolCallsFromMessage(message) {
|
|
817
|
+
if (!isRecord(message) || !Array.isArray(message.content))
|
|
818
|
+
return;
|
|
819
|
+
for (let contentIndex = 0; contentIndex < message.content.length; contentIndex += 1) {
|
|
820
|
+
const block = message.content[contentIndex];
|
|
821
|
+
if (isRecord(block) && block.type === "toolCall")
|
|
822
|
+
this.upsertPendingToolCall(block, contentIndex);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
upsertPendingToolCall(toolCall, contentIndex) {
|
|
826
|
+
const existingCallId = contentIndex === undefined ? undefined : this.pendingToolCallIdsByContentIndex.get(contentIndex);
|
|
827
|
+
const existingEntryId = existingCallId === undefined ? undefined : this.toolEntryIdsByCallId.get(existingCallId);
|
|
828
|
+
const toolCallId = isRecord(toolCall) && typeof toolCall.id === "string" ? toolCall.id : existingCallId ?? createId("tool-call");
|
|
829
|
+
if (contentIndex !== undefined)
|
|
830
|
+
this.pendingToolCallIdsByContentIndex.set(contentIndex, toolCallId);
|
|
831
|
+
if (existingCallId !== undefined && existingCallId !== toolCallId && existingEntryId !== undefined) {
|
|
832
|
+
this.toolEntryIdsByCallId.delete(existingCallId);
|
|
833
|
+
// B.7: if tool_execution_start arrived before toolcall_end finalised the
|
|
834
|
+
// call id, an orphan tool entry may already be registered under
|
|
835
|
+
// toolCallId. Reuse the pending entry and drop the orphan so a single
|
|
836
|
+
// logical tool call renders as a single tool entry.
|
|
837
|
+
const orphanEntryId = this.toolEntryIdsByCallId.get(toolCallId);
|
|
838
|
+
this.toolEntryIdsByCallId.set(toolCallId, existingEntryId);
|
|
839
|
+
const existing = this.findEntry(existingEntryId);
|
|
840
|
+
if (existing?.kind === "tool")
|
|
841
|
+
existing.toolCallId = toolCallId;
|
|
842
|
+
if (orphanEntryId !== undefined && orphanEntryId !== existingEntryId) {
|
|
843
|
+
this.removeToolEntryOrphan(orphanEntryId);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const toolName = isRecord(toolCall) && typeof toolCall.name === "string" ? toolCall.name : undefined;
|
|
847
|
+
const argsText = isRecord(toolCall) && "arguments" in toolCall ? stringifyUnknown(toolCall.arguments) : undefined;
|
|
848
|
+
this.upsertToolEntry(toolCallId, {
|
|
849
|
+
...(toolName === undefined ? {} : { toolName }),
|
|
850
|
+
...(argsText === undefined ? {} : { argsText }),
|
|
851
|
+
status: "running",
|
|
852
|
+
});
|
|
853
|
+
}
|
|
431
854
|
upsertToolEntry(toolCallId, update) {
|
|
432
855
|
const existingId = this.toolEntryIdsByCallId.get(toolCallId);
|
|
433
856
|
const existing = existingId ? this.findEntry(existingId) : undefined;
|
|
434
857
|
if (existing?.kind === "tool") {
|
|
435
858
|
existing.toolName = update.toolName ?? existing.toolName;
|
|
436
859
|
existing.argsText = update.argsText ?? existing.argsText;
|
|
437
|
-
|
|
860
|
+
// A.2: an update carrying empty output (e.g. a partial
|
|
861
|
+
// tool_execution_update with content:[]) must not erase output already
|
|
862
|
+
// shown to the user; only non-empty output replaces the existing value.
|
|
863
|
+
const nextOutput = update.output ?? existing.output;
|
|
864
|
+
existing.output = nextOutput.length > 0 ? nextOutput : (existing.output ?? "");
|
|
438
865
|
if ("images" in update)
|
|
439
866
|
existing.images = update.images;
|
|
440
867
|
if ("details" in update)
|
|
@@ -462,10 +889,52 @@ export class AppSessionEventController {
|
|
|
462
889
|
}
|
|
463
890
|
clearCurrentAssistantState() {
|
|
464
891
|
this.currentAssistantEntryId = undefined;
|
|
892
|
+
this.currentAssistantTextBlockEntryId = undefined;
|
|
893
|
+
this.currentAssistantTextBlockStartLength = undefined;
|
|
894
|
+
this.currentAssistantTextBlockContentIndex = undefined;
|
|
465
895
|
this.currentThinkingEntryId = undefined;
|
|
466
896
|
this.assistantTextBuffer = "";
|
|
897
|
+
this.assistantTextBlocksByContentIndex.clear();
|
|
898
|
+
this.finalizedToolCallContentIndexes.clear();
|
|
899
|
+
this.pendingToolCallIdsByContentIndex.clear();
|
|
467
900
|
}
|
|
468
901
|
}
|
|
902
|
+
function partialToolCallAt(partial, contentIndex) {
|
|
903
|
+
if (!isRecord(partial) || !Array.isArray(partial.content))
|
|
904
|
+
return undefined;
|
|
905
|
+
const block = partial.content[contentIndex];
|
|
906
|
+
return isRecord(block) && block.type === "toolCall" ? block : undefined;
|
|
907
|
+
}
|
|
908
|
+
function assistantTextSnapshotForContentIndex(message, partial, contentIndex) {
|
|
909
|
+
if (contentIndex === undefined)
|
|
910
|
+
return undefined;
|
|
911
|
+
return assistantTextContentAt(message, contentIndex) ?? assistantTextContentAt(partial, contentIndex);
|
|
912
|
+
}
|
|
913
|
+
function assistantTextContentAt(value, contentIndex) {
|
|
914
|
+
if (!isRecord(value) || !Array.isArray(value.content))
|
|
915
|
+
return undefined;
|
|
916
|
+
const block = value.content[contentIndex];
|
|
917
|
+
return isRecord(block) && block.type === "text" && typeof block.text === "string" ? block.text : undefined;
|
|
918
|
+
}
|
|
919
|
+
function assistantStreamVisibleTextForCompleteBlock(text, hasVisibleTextBeforeBlock) {
|
|
920
|
+
let buffer = text;
|
|
921
|
+
let visibleText = "";
|
|
922
|
+
for (;;) {
|
|
923
|
+
const newlineIndex = buffer.indexOf("\n");
|
|
924
|
+
if (newlineIndex === -1)
|
|
925
|
+
break;
|
|
926
|
+
const line = buffer.slice(0, newlineIndex);
|
|
927
|
+
buffer = buffer.slice(newlineIndex + 1);
|
|
928
|
+
if (shouldDropAssistantStreamLine(line, hasVisibleTextBeforeBlock || visibleText.length > 0))
|
|
929
|
+
continue;
|
|
930
|
+
visibleText += `${line}\n`;
|
|
931
|
+
}
|
|
932
|
+
if (!buffer)
|
|
933
|
+
return visibleText;
|
|
934
|
+
if (shouldHoldAssistantStreamTail(buffer, hasVisibleTextBeforeBlock || visibleText.length > 0))
|
|
935
|
+
return visibleText;
|
|
936
|
+
return visibleText + buffer;
|
|
937
|
+
}
|
|
469
938
|
function shouldDropAssistantStreamLine(line, hasVisibleText) {
|
|
470
939
|
if (line.trim().length === 0 && !hasVisibleText)
|
|
471
940
|
return true;
|