pi-ui-extend 0.1.2 → 0.1.4
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 +14 -3
- package/bin/pix.mjs +24 -1
- package/dist/app/app.d.ts +0 -1
- package/dist/app/app.js +4 -7
- package/dist/app/cli.js +3 -1
- package/dist/app/clipboard.d.ts +2 -0
- package/dist/app/clipboard.js +54 -1
- package/dist/app/conversation-entry-renderer.d.ts +0 -1
- package/dist/app/conversation-entry-renderer.js +2 -6
- package/dist/app/conversation-tool-renderer.js +2 -3
- package/dist/app/conversation-viewport.d.ts +0 -1
- package/dist/app/conversation-viewport.js +0 -1
- package/dist/app/dcp-stats.js +143 -14
- package/dist/app/install.d.ts +10 -0
- package/dist/app/install.js +135 -0
- package/dist/app/mouse-controller.d.ts +6 -6
- package/dist/app/mouse-controller.js +19 -1
- package/dist/app/nerd-font-controller.d.ts +6 -0
- package/dist/app/nerd-font-controller.js +98 -17
- package/dist/app/render-controller.js +5 -4
- package/dist/app/startup-checks.js +10 -7
- package/dist/app/toast-controller.d.ts +5 -2
- package/dist/app/toast-controller.js +7 -4
- package/dist/app/toast-renderer.d.ts +3 -0
- package/dist/app/toast-renderer.js +72 -11
- package/dist/app/types.d.ts +8 -4
- package/dist/config.d.ts +0 -3
- package/dist/config.js +0 -79
- package/dist/default-pix-config.js +2 -2
- package/dist/markdown-format.js +18 -1
- package/dist/ui.d.ts +5 -1
- package/dist/ui.js +2 -2
- package/external/pi-tools-suite/README.md +4 -4
- package/external/pi-tools-suite/licenses/opencode-dynamic-context-pruning-AGPL-3.0.txt +619 -0
- package/external/pi-tools-suite/package.json +1 -1
- package/external/pi-tools-suite/src/config.ts +5 -1
- package/external/pi-tools-suite/src/{compress → dcp}/config.ts +10 -70
- package/external/pi-tools-suite/src/{compress → dcp}/index.ts +16 -66
- package/external/pi-tools-suite/src/dcp/ui.ts +45 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +3 -2
- package/external/pi-tools-suite/src/index.ts +1 -1
- package/external/pi-tools-suite/src/tool-descriptions.ts +1 -1
- package/package.json +1 -1
- package/external/pi-tools-suite/src/compress/dcp-tui-filter.ts +0 -498
- package/external/pi-tools-suite/src/compress/ui.ts +0 -308
- /package/external/pi-tools-suite/src/{compress → dcp}/commands.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/compress-tool.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/compression-blocks.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/prompts.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-candidates.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-compression-blocks.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-message-ids.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-metadata.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-nudge.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-tools.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner-types.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/pruner.ts +0 -0
- /package/external/pi-tools-suite/src/{compress → dcp}/state.ts +0 -0
|
@@ -1,498 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// DCP TUI Filter — strips DCP metadata tags from assistant chat messages
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// The DCP context handler injects markdown reference marker lines such as
|
|
5
|
-
// [dcp-id]: # (m001) plus <dcp-system-reminder> tags into the context copy
|
|
6
|
-
// sent to the LLM. The LLM sometimes echoes this metadata back. This module
|
|
7
|
-
// strips them from streaming/displayed assistant messages so the TUI stays clean.
|
|
8
|
-
//
|
|
9
|
-
// We filter ASSISTANT messages only. User and toolResult messages are not
|
|
10
|
-
// mutated because message_end replacement persists to the session — stripping
|
|
11
|
-
// real content that happens to contain DCP-like tags would be irreversible.
|
|
12
|
-
// DCP tags in user/toolResult messages are injected only into context copies
|
|
13
|
-
// (by injectMessageIds) and are never stored, so they never reach the TUI.
|
|
14
|
-
//
|
|
15
|
-
// Tags are re-injected on every context event (before each LLM call),
|
|
16
|
-
// so stripping from stored messages is safe — the agent always sees them.
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
AssistantMessageComponent,
|
|
21
|
-
BashExecutionComponent,
|
|
22
|
-
CustomMessageComponent,
|
|
23
|
-
type ExtensionAPI,
|
|
24
|
-
ToolExecutionComponent,
|
|
25
|
-
UserMessageComponent,
|
|
26
|
-
} from "@mariozechner/pi-coding-agent"
|
|
27
|
-
|
|
28
|
-
// ---------------------------------------------------------------------------
|
|
29
|
-
// Regex patterns — broad opencode-style catch-all for ANY <dcp*> tag
|
|
30
|
-
// ---------------------------------------------------------------------------
|
|
31
|
-
|
|
32
|
-
/** Matches any paired DCP tag: <dcp-anything>...</dcp-anything> (non-greedy). */
|
|
33
|
-
const DCP_PAIRED_TAG_RE = /<dcp[^>]*>[\s\S]*?<\/dcp[^>]*>/gi
|
|
34
|
-
|
|
35
|
-
/** Matches any unpaired/orphan opening or closing DCP tag fragment. */
|
|
36
|
-
const DCP_UNPAIRED_TAG_RE = /<\/?dcp[^>]*>/gi
|
|
37
|
-
|
|
38
|
-
/** Matches markdown reference marker lines such as `[dcp-id]: # (m156)`. */
|
|
39
|
-
const DCP_MARKDOWN_REF_LINE_RE = /^\s*\[dcp[^\]]*\]:\s*#(?:\s*\([^)]*\)|\s+"[^"]*"|\s+'[^']*')?(?:\s+priority=(?:low|medium|high))?\s*$/gim
|
|
40
|
-
|
|
41
|
-
/** Matches malformed/truncated DCP markdown marker lines at line start. */
|
|
42
|
-
const DCP_MARKDOWN_REF_FRAGMENT_LINE_RE = /^\s*\[dcp[^\n]*$/gim
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Streaming only: matches a DCP markdown reference marker line that has started
|
|
46
|
-
* but may not be complete yet, e.g. `[dcp-id]:`, `[dcp-id]: # (m023`, or
|
|
47
|
-
* legacy `[dcp-id]: # (m023) priority=` at the end of the current assistant text.
|
|
48
|
-
*/
|
|
49
|
-
const DCP_MARKDOWN_REF_TO_END_RE = /(^|\n)\s*\[dcp[^\n]*$/i
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Streaming only: matches an open DCP tag followed by content to the end
|
|
53
|
-
* of text, where the closing tag hasn't arrived yet.
|
|
54
|
-
* e.g. `<dcp-id>m156` or `<dcp-foo>some content here`
|
|
55
|
-
*
|
|
56
|
-
* IMPORTANT: must be applied BEFORE DCP_UNPAIRED_TAG_RE in streaming mode,
|
|
57
|
-
* because unpaired would strip just the `<dcp-id>` part, leaving `m156`
|
|
58
|
-
* stranded in the output.
|
|
59
|
-
*/
|
|
60
|
-
const DCP_OPEN_TAG_TO_END_RE = /<dcp[^>]*>[\s\S]*$/gi
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Streaming only: matches an incomplete DCP tag prefix at the end of text.
|
|
64
|
-
*
|
|
65
|
-
* Providers can split a tag at any byte/token boundary, so the UI may briefly
|
|
66
|
-
* see `<`, `<d`, `<dc`, `<dcp`, or `<dcp-id` before the full opening tag is
|
|
67
|
-
* available. Hide those suffixes during streaming only.
|
|
68
|
-
*/
|
|
69
|
-
const DCP_INCOMPLETE_OPEN_RE = /(?:<|<\/?d|<\/?dc|<\/?dcp(?:[^<>\n]*)?)$/i
|
|
70
|
-
|
|
71
|
-
/** Case-insensitive quick check — is there anything DCP-shaped in this text? */
|
|
72
|
-
const DCP_QUICK_CHECK_RE = /<\/?d(?:c(?:p)?)?|\[dcp|dcp/i
|
|
73
|
-
|
|
74
|
-
type StreamTextKind = "text" | "thinking"
|
|
75
|
-
|
|
76
|
-
const RENDER_PATCH_FLAG = Symbol.for("pi-tools-suite.dcpTuiFilter.renderPatch")
|
|
77
|
-
const DISPLAY_RENDER_PATCH_FLAG = Symbol.for("pi-tools-suite.dcpTuiFilter.displayRenderPatch")
|
|
78
|
-
|
|
79
|
-
const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~]|\][^\x07]*(?:\x07|\x1B\\))/g
|
|
80
|
-
const RENDERED_DCP_METADATA_RE = /\[dcp[^\]]*\]:\s*#|<\/?dcp-system-reminder\b/i
|
|
81
|
-
const RENDERED_DCP_CONTINUATION_RE = /^(?:#\s*)?\(?(?:m\d+|b\d+)\)?(?:\s+priority=(?:low|medium|high)\)?)?$|^priority=?$|^(?:low|medium|high)\)?$/i
|
|
82
|
-
|
|
83
|
-
// ---------------------------------------------------------------------------
|
|
84
|
-
// Tag stripping
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Strip all DCP metadata tags from a text string.
|
|
89
|
-
*
|
|
90
|
-
* Uses two broad regexes (matching opencode's approach):
|
|
91
|
-
* 1. Paired tags: <dcp-anything>content</dcp-anything>
|
|
92
|
-
* 2. Unpaired/orphan: any remaining <dcp...> or </dcp...> fragments
|
|
93
|
-
*
|
|
94
|
-
* During streaming, also hides:
|
|
95
|
-
* - Open DCP tags with trailing content (no closing tag yet)
|
|
96
|
-
* - Incomplete tag prefixes at end of text
|
|
97
|
-
*
|
|
98
|
-
* Returns the cleaned text, or the original if no tags were found.
|
|
99
|
-
*/
|
|
100
|
-
export function stripDcpTags(
|
|
101
|
-
text: string,
|
|
102
|
-
options: { streaming?: boolean } = {},
|
|
103
|
-
): string {
|
|
104
|
-
if (!text || !DCP_QUICK_CHECK_RE.test(text)) return text
|
|
105
|
-
|
|
106
|
-
// 1. Strip markdown reference marker lines, then fully paired XML tags
|
|
107
|
-
let cleaned = text
|
|
108
|
-
.replace(DCP_MARKDOWN_REF_LINE_RE, "")
|
|
109
|
-
.replace(DCP_MARKDOWN_REF_FRAGMENT_LINE_RE, "")
|
|
110
|
-
.replace(DCP_PAIRED_TAG_RE, "")
|
|
111
|
-
|
|
112
|
-
if (options.streaming) {
|
|
113
|
-
// 2. In streaming: strip open tags to end of text BEFORE unpaired,
|
|
114
|
-
// so that `<dcp-id>m156` is removed as a whole unit rather than
|
|
115
|
-
// having unpaired strip just `<dcp-id>` and leaving `m156` stranded
|
|
116
|
-
cleaned = cleaned
|
|
117
|
-
.replace(DCP_MARKDOWN_REF_TO_END_RE, "$1")
|
|
118
|
-
.replace(DCP_OPEN_TAG_TO_END_RE, "")
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 3. Strip any remaining orphan opening/closing tags
|
|
122
|
-
cleaned = cleaned.replace(DCP_UNPAIRED_TAG_RE, "")
|
|
123
|
-
|
|
124
|
-
if (options.streaming) {
|
|
125
|
-
// 4. Strip incomplete tag prefixes at end of streaming text
|
|
126
|
-
cleaned = cleaned.replace(DCP_INCOMPLETE_OPEN_RE, "")
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return cleaned
|
|
130
|
-
.replace(/\n{3,}/g, "\n\n") // collapse excessive blank lines
|
|
131
|
-
.trimEnd()
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function stripDcpRenderedLines(lines: string[]): string[] {
|
|
135
|
-
const cleaned: string[] = []
|
|
136
|
-
let droppingWrappedMetadata = false
|
|
137
|
-
|
|
138
|
-
for (const line of lines) {
|
|
139
|
-
const plain = line.replace(ANSI_RE, "").trim()
|
|
140
|
-
if (RENDERED_DCP_METADATA_RE.test(plain)) {
|
|
141
|
-
droppingWrappedMetadata = true
|
|
142
|
-
continue
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (droppingWrappedMetadata && RENDERED_DCP_CONTINUATION_RE.test(plain)) {
|
|
146
|
-
continue
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
droppingWrappedMetadata = false
|
|
150
|
-
cleaned.push(line)
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return cleaned
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// ---------------------------------------------------------------------------
|
|
157
|
-
// Content block helpers
|
|
158
|
-
// ---------------------------------------------------------------------------
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Strip DCP tags from a single content block (text or thinking).
|
|
162
|
-
* Returns [block, modified] tuple. Block is undefined if it should be dropped.
|
|
163
|
-
*/
|
|
164
|
-
function stripBlock(block: any, streaming = false): [any, boolean] {
|
|
165
|
-
if (!block || typeof block !== "object") return [block, false]
|
|
166
|
-
|
|
167
|
-
let modified = false
|
|
168
|
-
let next = block
|
|
169
|
-
|
|
170
|
-
// Text blocks
|
|
171
|
-
if (typeof block.text === "string") {
|
|
172
|
-
const cleaned = stripDcpTags(block.text, { streaming })
|
|
173
|
-
if (cleaned !== block.text) {
|
|
174
|
-
modified = true
|
|
175
|
-
if (cleaned.trim() === "" && block.type === "text") return [undefined, true]
|
|
176
|
-
next = { ...block, text: cleaned }
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Thinking blocks
|
|
181
|
-
if (typeof block.thinking === "string") {
|
|
182
|
-
const cleaned = stripDcpTags(block.thinking, { streaming })
|
|
183
|
-
if (cleaned !== block.thinking) {
|
|
184
|
-
modified = true
|
|
185
|
-
if (cleaned.trim() === "" && block.type === "thinking") return [undefined, true]
|
|
186
|
-
next = { ...next, thinking: cleaned }
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
// Delete stale signatures when content was modified. Providers verify
|
|
191
|
-
// textSignature/thinkingSignature against the exact content — a mismatch
|
|
192
|
-
// causes rejection. Mirrors the existing DCP metadata stripper pattern
|
|
193
|
-
// in pruner-metadata.ts:stripStaleDcpMetadataFromAssistantBlock.
|
|
194
|
-
if (modified) {
|
|
195
|
-
if ("textSignature" in next) {
|
|
196
|
-
const { textSignature: _, ...rest } = next
|
|
197
|
-
next = rest
|
|
198
|
-
}
|
|
199
|
-
if ("thinkingSignature" in next) {
|
|
200
|
-
const { thinkingSignature: _, ...rest } = next
|
|
201
|
-
next = rest
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return [next, modified]
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Strip DCP tags from an assistant message's content.
|
|
210
|
-
* Returns the cleaned message (shallow copy if modified, original if clean).
|
|
211
|
-
*/
|
|
212
|
-
function stripDcpFromAssistantMessage(message: any, streaming = false): any {
|
|
213
|
-
if (!message || typeof message !== "object") return message
|
|
214
|
-
|
|
215
|
-
const content = message.content
|
|
216
|
-
if (!content) return message
|
|
217
|
-
|
|
218
|
-
// String content
|
|
219
|
-
if (typeof content === "string") {
|
|
220
|
-
const cleaned = stripDcpTags(content, { streaming })
|
|
221
|
-
if (cleaned === content) return message
|
|
222
|
-
return { ...message, content: cleaned }
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Array content (Anthropic-style content blocks)
|
|
226
|
-
if (!Array.isArray(content)) return message
|
|
227
|
-
|
|
228
|
-
let modified = false
|
|
229
|
-
const newContent = content
|
|
230
|
-
.map((block: any) => {
|
|
231
|
-
const [cleaned, wasModified] = stripBlock(block, streaming)
|
|
232
|
-
if (wasModified) modified = true
|
|
233
|
-
return cleaned
|
|
234
|
-
})
|
|
235
|
-
.filter((block: any) => block !== undefined)
|
|
236
|
-
|
|
237
|
-
if (!modified) return message
|
|
238
|
-
return { ...message, content: newContent }
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Mutate the shallow event message object that Pi sends to TUI/listeners.
|
|
243
|
-
*
|
|
244
|
-
* `message_update` has no return-value replacement API, but Pi emits extension
|
|
245
|
-
* events before TUI listeners and passes the same shallow event message object
|
|
246
|
-
* onward. Replacing `message.content` here cleans the rendered stream without
|
|
247
|
-
* mutating the provider's raw partial/final assistant message object.
|
|
248
|
-
*/
|
|
249
|
-
function stripDcpFromAssistantMessageInPlace(message: any, streaming = false): boolean {
|
|
250
|
-
const cleaned = stripDcpFromAssistantMessage(message, streaming)
|
|
251
|
-
if (cleaned === message) return false
|
|
252
|
-
|
|
253
|
-
Object.assign(message as Record<string, unknown>, cleaned)
|
|
254
|
-
return true
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Patch Pi's assistant renderer as a final display-only safety net.
|
|
259
|
-
*
|
|
260
|
-
* Extension `message_update` handlers can sanitize the event message, but some
|
|
261
|
-
* live/render paths have historically shown raw text before or instead of the
|
|
262
|
-
* sanitized event object. Patching AssistantMessageComponent means any
|
|
263
|
-
* assistant text/thinking content is stripped immediately before TUI rendering,
|
|
264
|
-
* without mutating the stored session message or the provider-visible context.
|
|
265
|
-
*/
|
|
266
|
-
function registerAssistantRenderPatch(): void {
|
|
267
|
-
const prototype = AssistantMessageComponent?.prototype as Record<string | symbol, any> | undefined
|
|
268
|
-
if (!prototype || prototype[RENDER_PATCH_FLAG]) return
|
|
269
|
-
|
|
270
|
-
const originalUpdateContent = prototype.updateContent
|
|
271
|
-
if (typeof originalUpdateContent !== "function") return
|
|
272
|
-
|
|
273
|
-
Object.defineProperty(prototype, RENDER_PATCH_FLAG, {
|
|
274
|
-
value: true,
|
|
275
|
-
enumerable: false,
|
|
276
|
-
configurable: false,
|
|
277
|
-
})
|
|
278
|
-
|
|
279
|
-
prototype.updateContent = function updateContentWithDcpFilter(message: any): void {
|
|
280
|
-
const cleaned = stripDcpFromAssistantMessage(message, true)
|
|
281
|
-
return originalUpdateContent.call(this, cleaned)
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function patchRenderedLines(componentClass: unknown): void {
|
|
286
|
-
const prototype = (componentClass as any)?.prototype as Record<string | symbol, any> | undefined
|
|
287
|
-
if (!prototype || prototype[DISPLAY_RENDER_PATCH_FLAG]) return
|
|
288
|
-
|
|
289
|
-
const originalRender = prototype.render
|
|
290
|
-
if (typeof originalRender !== "function") return
|
|
291
|
-
|
|
292
|
-
Object.defineProperty(prototype, DISPLAY_RENDER_PATCH_FLAG, {
|
|
293
|
-
value: true,
|
|
294
|
-
enumerable: false,
|
|
295
|
-
configurable: false,
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
prototype.render = function renderWithDcpLineFilter(width: number): string[] {
|
|
299
|
-
const lines = originalRender.call(this, width)
|
|
300
|
-
return Array.isArray(lines) ? stripDcpRenderedLines(lines) : lines
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
function registerDisplayRenderPatches(): void {
|
|
305
|
-
patchRenderedLines(UserMessageComponent)
|
|
306
|
-
patchRenderedLines(CustomMessageComponent)
|
|
307
|
-
patchRenderedLines(ToolExecutionComponent)
|
|
308
|
-
patchRenderedLines(BashExecutionComponent)
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function getAssistantBlockString(message: any, contentIndex: number, kind: StreamTextKind): string | undefined {
|
|
312
|
-
const content = message?.content
|
|
313
|
-
if (!Array.isArray(content)) return undefined
|
|
314
|
-
|
|
315
|
-
const block = content[contentIndex]
|
|
316
|
-
if (!block || typeof block !== "object") return undefined
|
|
317
|
-
|
|
318
|
-
const value = block[kind]
|
|
319
|
-
return typeof value === "string" ? value : undefined
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
function streamStateKey(kind: StreamTextKind, contentIndex: number): string {
|
|
323
|
-
return `${kind}:${contentIndex}`
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* Keep low-level streaming events in sync with the sanitized display message.
|
|
328
|
-
*
|
|
329
|
-
* The Pi TUI reads `event.message`, but RPC/proxy clients may render from
|
|
330
|
-
* `assistantMessageEvent.delta` or `.partial`. If those fields keep the raw
|
|
331
|
-
* provider chunks, DCP tags can still flash even though the TUI message copy is
|
|
332
|
-
* clean. Mutate nested assistant event fields in place: AgentSession passes the
|
|
333
|
-
* same nested `assistantMessageEvent` object to extension handlers and TUI
|
|
334
|
-
* listeners, but replacing the top-level extension event object does not flow
|
|
335
|
-
* back to the original AgentSession event.
|
|
336
|
-
*/
|
|
337
|
-
function sanitizeAssistantMessageEvent(event: any, streamTextByKey: Map<string, string>): void {
|
|
338
|
-
const assistantEvent = event?.assistantMessageEvent
|
|
339
|
-
if (!assistantEvent || typeof assistantEvent !== "object") return
|
|
340
|
-
|
|
341
|
-
const rawPartial = assistantEvent.partial
|
|
342
|
-
const cleanedPartial = stripDcpFromAssistantMessage(rawPartial, true)
|
|
343
|
-
|
|
344
|
-
if (cleanedPartial !== rawPartial) {
|
|
345
|
-
assistantEvent.partial = cleanedPartial
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const type = assistantEvent.type
|
|
349
|
-
if (
|
|
350
|
-
(type === "text_start" || type === "text_delta" || type === "text_end") &&
|
|
351
|
-
typeof assistantEvent.contentIndex === "number"
|
|
352
|
-
) {
|
|
353
|
-
sanitizeTextStreamEvent(
|
|
354
|
-
assistantEvent,
|
|
355
|
-
cleanedPartial,
|
|
356
|
-
"text",
|
|
357
|
-
streamTextByKey,
|
|
358
|
-
)
|
|
359
|
-
} else if (
|
|
360
|
-
(type === "thinking_start" || type === "thinking_delta" || type === "thinking_end") &&
|
|
361
|
-
typeof assistantEvent.contentIndex === "number"
|
|
362
|
-
) {
|
|
363
|
-
sanitizeTextStreamEvent(
|
|
364
|
-
assistantEvent,
|
|
365
|
-
cleanedPartial,
|
|
366
|
-
"thinking",
|
|
367
|
-
streamTextByKey,
|
|
368
|
-
)
|
|
369
|
-
} else if (type === "start") {
|
|
370
|
-
streamTextByKey.clear()
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function sanitizeTextStreamEvent(
|
|
375
|
-
assistantEvent: any,
|
|
376
|
-
cleanedPartial: any,
|
|
377
|
-
kind: StreamTextKind,
|
|
378
|
-
streamTextByKey: Map<string, string>,
|
|
379
|
-
): void {
|
|
380
|
-
const contentIndex = assistantEvent.contentIndex as number
|
|
381
|
-
const key = streamStateKey(kind, contentIndex)
|
|
382
|
-
const cleanedFullText = getAssistantBlockString(cleanedPartial, contentIndex, kind) ?? ""
|
|
383
|
-
|
|
384
|
-
if (assistantEvent.type === `${kind}_start`) {
|
|
385
|
-
streamTextByKey.set(key, cleanedFullText)
|
|
386
|
-
return
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
if (assistantEvent.type === `${kind}_delta`) {
|
|
390
|
-
const previousText = streamTextByKey.get(key) ?? ""
|
|
391
|
-
const cleanedDelta = cleanedFullText.startsWith(previousText)
|
|
392
|
-
? cleanedFullText.slice(previousText.length)
|
|
393
|
-
: cleanedFullText
|
|
394
|
-
|
|
395
|
-
streamTextByKey.set(key, cleanedFullText)
|
|
396
|
-
|
|
397
|
-
if (cleanedDelta !== assistantEvent.delta) {
|
|
398
|
-
assistantEvent.delta = cleanedDelta
|
|
399
|
-
}
|
|
400
|
-
return
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (assistantEvent.type === `${kind}_end`) {
|
|
404
|
-
streamTextByKey.set(key, cleanedFullText)
|
|
405
|
-
if (typeof assistantEvent.content === "string") {
|
|
406
|
-
const cleanedContent = stripDcpTags(assistantEvent.content)
|
|
407
|
-
if (cleanedContent !== assistantEvent.content) {
|
|
408
|
-
assistantEvent.content = cleanedContent
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* Best-effort in-memory cleanup for assistant messages that were persisted by
|
|
416
|
-
* older filter versions before `message_end` stripping existed/worked. This is
|
|
417
|
-
* intentionally assistant-only for the same safety reasons as the live filter.
|
|
418
|
-
*/
|
|
419
|
-
function scrubAssistantMessagesInSessionHistory(sessionManager: any): void {
|
|
420
|
-
const entries = sessionManager?.getEntries?.()
|
|
421
|
-
if (!Array.isArray(entries)) return
|
|
422
|
-
|
|
423
|
-
for (const entry of entries) {
|
|
424
|
-
if (!entry || entry.type !== "message") continue
|
|
425
|
-
|
|
426
|
-
const msg = entry.message
|
|
427
|
-
if (!msg || msg.role !== "assistant") continue
|
|
428
|
-
|
|
429
|
-
const cleaned = stripDcpFromAssistantMessage(msg)
|
|
430
|
-
if (cleaned !== msg) {
|
|
431
|
-
Object.assign(msg as Record<string, unknown>, cleaned)
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
// ---------------------------------------------------------------------------
|
|
437
|
-
// Extension hook
|
|
438
|
-
// ---------------------------------------------------------------------------
|
|
439
|
-
|
|
440
|
-
/**
|
|
441
|
-
* Register the DCP TUI filter on the given extension API.
|
|
442
|
-
*
|
|
443
|
-
* Hooks into both streaming `message_update` and finalized `message_end`:
|
|
444
|
-
* - `message_update` prevents tags from flashing in the TUI while tokens stream
|
|
445
|
-
* - `message_end` keeps stored assistant messages clean
|
|
446
|
-
*
|
|
447
|
-
* We only filter assistant messages because:
|
|
448
|
-
* - DCP tags in user/toolResult are only added to context copies, never stored
|
|
449
|
-
* - Mutating user/toolResult via message_end persists permanently and could
|
|
450
|
-
* corrupt legitimate content that happens to contain DCP-like text
|
|
451
|
-
* - The LLM is the only source of echoed DCP tags in stored messages
|
|
452
|
-
*/
|
|
453
|
-
export function registerTuiFilter(pi: ExtensionAPI): void {
|
|
454
|
-
registerAssistantRenderPatch()
|
|
455
|
-
registerDisplayRenderPatches()
|
|
456
|
-
|
|
457
|
-
const streamTextByKey = new Map<string, string>()
|
|
458
|
-
|
|
459
|
-
pi.on("session_start", async (_event, ctx) => {
|
|
460
|
-
scrubAssistantMessagesInSessionHistory(ctx.sessionManager)
|
|
461
|
-
streamTextByKey.clear()
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
pi.on("message_start", async (event, _ctx) => {
|
|
465
|
-
const role: string = (event.message as any)?.role ?? ""
|
|
466
|
-
if (role === "assistant") {
|
|
467
|
-
streamTextByKey.clear()
|
|
468
|
-
}
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
pi.on("message_update", async (event, _ctx) => {
|
|
472
|
-
const msg = event.message
|
|
473
|
-
if (!msg || typeof msg !== "object") return
|
|
474
|
-
|
|
475
|
-
const role: string = (msg as any).role ?? ""
|
|
476
|
-
if (role !== "assistant") return
|
|
477
|
-
|
|
478
|
-
stripDcpFromAssistantMessageInPlace(msg, true)
|
|
479
|
-
sanitizeAssistantMessageEvent(event, streamTextByKey)
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
pi.on("message_end", async (event, _ctx) => {
|
|
483
|
-
streamTextByKey.clear()
|
|
484
|
-
|
|
485
|
-
const msg = event.message
|
|
486
|
-
if (!msg || typeof msg !== "object") return
|
|
487
|
-
|
|
488
|
-
// Assistant-only: DCP tags only appear in stored assistant messages
|
|
489
|
-
// when the LLM echoes them back from the context
|
|
490
|
-
const role: string = (msg as any).role ?? ""
|
|
491
|
-
if (role !== "assistant") return
|
|
492
|
-
|
|
493
|
-
const cleaned = stripDcpFromAssistantMessage(msg)
|
|
494
|
-
if (cleaned !== msg) {
|
|
495
|
-
return { message: cleaned }
|
|
496
|
-
}
|
|
497
|
-
})
|
|
498
|
-
}
|