pi-ui-extend 0.1.48 → 0.1.49
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/dist/app/rendering/conversation-tool-renderer.js +0 -1
- package/dist/app/rendering/tool-block-renderer.d.ts +1 -2
- package/dist/app/rendering/tool-block-renderer.js +2 -42
- package/dist/tool-renderers/compress.js +7 -28
- package/dist/tool-renderers/index.js +8 -0
- package/dist/tool-renderers/question.js +1 -1
- package/dist/tool-renderers/search.d.ts +2 -0
- package/dist/tool-renderers/search.js +15 -0
- package/dist/tool-renderers/skill.js +1 -1
- package/dist/tool-renderers/types.d.ts +0 -9
- package/dist/tool-renderers/utils.js +1 -1
- package/external/pi-tools-suite/src/codex-reasoning-fix/index.ts +121 -72
- package/external/pi-tools-suite/src/dcp/index.ts +23 -3
- package/package.json +1 -1
|
@@ -31,7 +31,6 @@ export function renderConversationToolEntry(entry, width, options) {
|
|
|
31
31
|
id: entry.id,
|
|
32
32
|
toolName,
|
|
33
33
|
headerArgs: display.headerArgs,
|
|
34
|
-
headerArgsSegments: display.headerArgsSegments,
|
|
35
34
|
bodyLineStyles: display.bodyLineStyles,
|
|
36
35
|
bodyStyle: display.bodyStyle,
|
|
37
36
|
preserveAnsi: display.preserveAnsi,
|
|
@@ -2,13 +2,12 @@ import { type ResolvedToolRule } from "../../config.js";
|
|
|
2
2
|
import type { ToolBodySyntaxHighlights } from "../../syntax-highlight.js";
|
|
3
3
|
import type { Theme } from "../../theme.js";
|
|
4
4
|
import type { RenderedLine } from "../types.js";
|
|
5
|
-
import type { ToolBodyLineStyle
|
|
5
|
+
import type { ToolBodyLineStyle } from "../../tool-renderers/types.js";
|
|
6
6
|
export type ToolBlockEntry = {
|
|
7
7
|
id: string;
|
|
8
8
|
toolName: string;
|
|
9
9
|
headerLabel?: string | undefined;
|
|
10
10
|
headerArgs?: string | undefined;
|
|
11
|
-
headerArgsSegments?: readonly ToolHeaderSegment[] | undefined;
|
|
12
11
|
bodyLineStyles?: readonly ToolBodyLineStyle[] | undefined;
|
|
13
12
|
bodyStyle?: "diff" | undefined;
|
|
14
13
|
preserveAnsi?: boolean | undefined;
|
|
@@ -19,7 +19,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
19
19
|
const expanded = entry.expanded;
|
|
20
20
|
const stateIcon = toolStatusIcon(entry);
|
|
21
21
|
const toolColor = options.headerColorOverride ?? resolveColor(rule.color, colors);
|
|
22
|
-
const toolOutputColor =
|
|
22
|
+
const toolOutputColor = toolColor;
|
|
23
23
|
const headerLabel = (entry.headerLabel ?? entry.toolName).toLowerCase();
|
|
24
24
|
const bg = options.backgroundOverride;
|
|
25
25
|
const applyBackground = bg ? (lines) => { for (const line of lines)
|
|
@@ -31,7 +31,6 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
31
31
|
const target = { kind: "tool", id: entry.id };
|
|
32
32
|
const showGutter = options.showGutter ?? true;
|
|
33
33
|
const header = clippedHeaderArgs ? `${headerPrefix} ${clippedHeaderArgs}` : headerPrefix;
|
|
34
|
-
const headerArgsStart = clippedHeaderArgs ? headerPrefix.length + 1 : header.length;
|
|
35
34
|
const headerLine = {
|
|
36
35
|
text: header,
|
|
37
36
|
target,
|
|
@@ -40,7 +39,6 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
40
39
|
segments: [
|
|
41
40
|
{ start: 0, end: stateIcon.length, foreground: toolStatusIconColor(entry, colors), bold: true },
|
|
42
41
|
{ start: stateIcon.length, end: headerPrefix.length, bold: true },
|
|
43
|
-
...headerArgsStyledSegments(headerArgsStart, clippedHeaderArgs.length, entry.headerArgsSegments, colors),
|
|
44
42
|
],
|
|
45
43
|
};
|
|
46
44
|
const headerLines = [headerLine];
|
|
@@ -88,10 +86,7 @@ export function renderToolBlock(entry, rule, width, colors, options = {}) {
|
|
|
88
86
|
}
|
|
89
87
|
function renderCollapsedPreviewLines(entry, body, rule, width, target, color, colors, hasLspDiagnostics, showGutter) {
|
|
90
88
|
const preview = previewBodyText(body, rule.direction, rule.previewLines);
|
|
91
|
-
|
|
92
|
-
// collapsed body here is the raw output only. Applying bodyLineStyles would misalign and
|
|
93
|
-
// incorrectly dim the first N output lines, so we do not pass it to the collapsed preview.
|
|
94
|
-
const allPreviewLines = renderToolBodyLines(preview.text, width, target, color, entry.bodyStyle, colors, undefined, entry.bodyWrap, hasLspDiagnostics, undefined, entry.preserveAnsi, showGutter, { rawLineOffset: preview.rawLineOffset, bodyEndsAfterText: !preview.omittedAfter });
|
|
89
|
+
const allPreviewLines = renderToolBodyLines(preview.text, width, target, color, entry.bodyStyle, colors, undefined, entry.bodyWrap, hasLspDiagnostics, entry.bodyLineStyles, entry.preserveAnsi, showGutter, { rawLineOffset: preview.rawLineOffset, bodyEndsAfterText: !preview.omittedAfter });
|
|
95
90
|
const overflow = preview.omittedBefore || preview.omittedAfter || allPreviewLines.length > rule.previewLines;
|
|
96
91
|
if (!overflow)
|
|
97
92
|
return allPreviewLines;
|
|
@@ -487,38 +482,3 @@ function diffLineStyle(line, colors) {
|
|
|
487
482
|
function formatToolHeaderArgs(argsText) {
|
|
488
483
|
return sanitizeText(argsText ?? "").replace(/\n+/g, " ").trim();
|
|
489
484
|
}
|
|
490
|
-
function headerArgsStyledSegments(headerArgsStart, clippedLength, customSegments, colors) {
|
|
491
|
-
if (clippedLength <= 0)
|
|
492
|
-
return [];
|
|
493
|
-
if (!customSegments || customSegments.length === 0)
|
|
494
|
-
return [{ start: headerArgsStart, end: headerArgsStart + clippedLength, foreground: colors.muted }];
|
|
495
|
-
const segments = [];
|
|
496
|
-
let offset = 0;
|
|
497
|
-
const clippedCustomSegments = customSegments
|
|
498
|
-
.map((segment) => clippedHeaderSegment(segment, clippedLength))
|
|
499
|
-
.filter((segment) => segment !== undefined)
|
|
500
|
-
.sort((left, right) => left.start - right.start || left.end - right.end);
|
|
501
|
-
for (const segment of clippedCustomSegments) {
|
|
502
|
-
const start = Math.max(offset, segment.start);
|
|
503
|
-
if (start >= segment.end)
|
|
504
|
-
continue;
|
|
505
|
-
if (start > offset)
|
|
506
|
-
segments.push({ start: headerArgsStart + offset, end: headerArgsStart + start, foreground: colors.muted });
|
|
507
|
-
segments.push({
|
|
508
|
-
...segment,
|
|
509
|
-
start: headerArgsStart + start,
|
|
510
|
-
end: headerArgsStart + segment.end,
|
|
511
|
-
});
|
|
512
|
-
offset = segment.end;
|
|
513
|
-
}
|
|
514
|
-
if (offset < clippedLength)
|
|
515
|
-
segments.push({ start: headerArgsStart + offset, end: headerArgsStart + clippedLength, foreground: colors.muted });
|
|
516
|
-
return segments;
|
|
517
|
-
}
|
|
518
|
-
function clippedHeaderSegment(segment, clippedLength) {
|
|
519
|
-
const start = Math.max(0, Math.min(clippedLength, segment.start));
|
|
520
|
-
const end = Math.max(start, Math.min(clippedLength, segment.end));
|
|
521
|
-
if (end <= start)
|
|
522
|
-
return undefined;
|
|
523
|
-
return { ...segment, start, end };
|
|
524
|
-
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatCompactProgressBar } from "../context-progress-bar.js";
|
|
2
2
|
import { parseArgsText, stringArg } from "./utils.js";
|
|
3
3
|
export const renderCompressTool = (input) => {
|
|
4
4
|
const topic = stringArg(input, ["topic"]);
|
|
@@ -6,7 +6,6 @@ export const renderCompressTool = (input) => {
|
|
|
6
6
|
const header = joinHeaderParts(topic ? { text: topic } : undefined, summary) ?? { text: "" };
|
|
7
7
|
return {
|
|
8
8
|
...(header.text ? { headerArgs: header.text } : {}),
|
|
9
|
-
...((header.segments?.length ?? 0) > 0 ? { headerArgsSegments: header.segments } : {}),
|
|
10
9
|
collapsedBody: "",
|
|
11
10
|
expandedText: fullCompressResponse(input.output, input.status),
|
|
12
11
|
};
|
|
@@ -20,14 +19,14 @@ function formatCompressSummary(input) {
|
|
|
20
19
|
const parsed = parseArgsText(output);
|
|
21
20
|
if (!isRecord(parsed))
|
|
22
21
|
return { text: oneLine(output) };
|
|
23
|
-
return formatCompressSuccess(parsed
|
|
22
|
+
return formatCompressSuccess(parsed);
|
|
24
23
|
}
|
|
25
24
|
function fullCompressResponse(output, status) {
|
|
26
25
|
if (output.trim())
|
|
27
26
|
return output.trimEnd();
|
|
28
27
|
return status === "running" ? "running…" : "(empty)";
|
|
29
28
|
}
|
|
30
|
-
function formatCompressSuccess(result
|
|
29
|
+
function formatCompressSuccess(result) {
|
|
31
30
|
const tokensSaved = numberValue(result.tokensSaved);
|
|
32
31
|
const contextPercent = numberValue(result.contextPercent);
|
|
33
32
|
const contextTokens = numberValue(result.contextTokens);
|
|
@@ -43,7 +42,7 @@ function formatCompressSuccess(result, input) {
|
|
|
43
42
|
const barPercent = compressedContextPercent ?? contextPercent;
|
|
44
43
|
const parts = [
|
|
45
44
|
{ text: tokensSaved != null ? `saved ${formatCompactNumber(tokensSaved)}` : "compressed" },
|
|
46
|
-
barPercent != null ? progressPart(barPercent,
|
|
45
|
+
barPercent != null ? progressPart(barPercent, originalContextTokens) : undefined,
|
|
47
46
|
compressedContextPercent != null && contextPercent != null ? { text: `context ${formatPercent(contextPercent)}` } : undefined,
|
|
48
47
|
itemCount != null ? { text: `${itemCount} ${plural(itemCount, "item")}` } : undefined,
|
|
49
48
|
summaryTokens != null ? { text: `${formatCompactNumber(summaryTokens)} summary tokens` } : undefined,
|
|
@@ -53,17 +52,9 @@ function formatCompressSuccess(result, input) {
|
|
|
53
52
|
].filter((part) => Boolean(part));
|
|
54
53
|
return joinHeaderParts(...parts) ?? { text: "compressed" };
|
|
55
54
|
}
|
|
56
|
-
function progressPart(percent,
|
|
55
|
+
function progressPart(percent, previousContextTokens) {
|
|
57
56
|
const text = `${formatCompactProgressBar(percent)} ${formatPercent(percent)}${previousContextTokens != null ? ` of ${formatCompactNumber(previousContextTokens)}` : ""}`;
|
|
58
|
-
|
|
59
|
-
return { text };
|
|
60
|
-
return {
|
|
61
|
-
text,
|
|
62
|
-
segments: compactProgressBarSegments(0, percent, {
|
|
63
|
-
fill: input.toolColor ?? input.colors.muted,
|
|
64
|
-
track: input.colors.statusDotBase,
|
|
65
|
-
}),
|
|
66
|
-
};
|
|
57
|
+
return { text };
|
|
67
58
|
}
|
|
68
59
|
function inferContextTokens(contextPercent, contextWindow) {
|
|
69
60
|
if (contextPercent == null || contextWindow == null || contextWindow <= 0)
|
|
@@ -80,19 +71,7 @@ function joinHeaderParts(...parts) {
|
|
|
80
71
|
if (defined.length === 0)
|
|
81
72
|
return undefined;
|
|
82
73
|
const separator = " · ";
|
|
83
|
-
|
|
84
|
-
let offset = 0;
|
|
85
|
-
for (const [index, part] of defined.entries()) {
|
|
86
|
-
if (index > 0)
|
|
87
|
-
offset += separator.length;
|
|
88
|
-
if (part.segments) {
|
|
89
|
-
for (const segment of part.segments) {
|
|
90
|
-
segments.push({ ...segment, start: offset + segment.start, end: offset + segment.end });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
offset += part.text.length;
|
|
94
|
-
}
|
|
95
|
-
return { text: defined.map((part) => part.text).join(separator), segments };
|
|
74
|
+
return { text: defined.map((part) => part.text).join(separator) };
|
|
96
75
|
}
|
|
97
76
|
function numberValue(value) {
|
|
98
77
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
@@ -4,6 +4,7 @@ import { renderCompressTool } from "./compress.js";
|
|
|
4
4
|
import { renderQuestionTool } from "./question.js";
|
|
5
5
|
import { renderReadTool } from "./read.js";
|
|
6
6
|
import { renderRepoTool } from "./repo.js";
|
|
7
|
+
import { renderSearchTool } from "./search.js";
|
|
7
8
|
import { renderShellTool } from "./shell.js";
|
|
8
9
|
import { applySkillReadDisplay } from "./skill.js";
|
|
9
10
|
import { renderSubagentsTool } from "./subagents.js";
|
|
@@ -25,6 +26,13 @@ const TOOL_RENDERERS = {
|
|
|
25
26
|
Write: renderWriteTool,
|
|
26
27
|
ast_grep: renderAstTool,
|
|
27
28
|
ast_apply: renderAstTool,
|
|
29
|
+
grep: renderSearchTool,
|
|
30
|
+
Grep: renderSearchTool,
|
|
31
|
+
rg: renderSearchTool,
|
|
32
|
+
Glob: renderSearchTool,
|
|
33
|
+
glob: renderSearchTool,
|
|
34
|
+
find: renderSearchTool,
|
|
35
|
+
Find: renderSearchTool,
|
|
28
36
|
web_search: renderWebSearchTool,
|
|
29
37
|
web_fetch: renderWebFetchTool,
|
|
30
38
|
todo: renderTodoTool,
|
|
@@ -16,7 +16,7 @@ export const renderQuestionTool = (input) => {
|
|
|
16
16
|
return {
|
|
17
17
|
headerArgs: formatHeader(questions),
|
|
18
18
|
collapsedBody: resultText,
|
|
19
|
-
...expandedTextFromParts({ text: questionsText
|
|
19
|
+
...expandedTextFromParts({ text: questionsText }, { text: input.isError ? `error\n${resultText}` : resultText }),
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
22
|
function questionItems(value) {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { argsRecord, formatArgsInline } from "./utils.js";
|
|
2
|
+
// Search tools (grep/Glob/find) show their arguments inline in the header. Rendering the
|
|
3
|
+
// arguments again as a muted body block (as defaultToolRender does) would dim the first output
|
|
4
|
+
// lines and duplicate the header, so these tools render the output only — like repo_* tools.
|
|
5
|
+
const PREFERRED_KEYS = ["pattern", "path", "glob", "type", "output_mode", "output_line_limit", "case_sensitive", "regex", "multiline", "-n", "context", "head_limit", "max_results"];
|
|
6
|
+
export const renderSearchTool = (input) => {
|
|
7
|
+
const args = argsRecord(input);
|
|
8
|
+
const result = input.output || (input.status === "running" ? "running…" : "(empty)");
|
|
9
|
+
const headerArgs = args ? formatArgsInline(args, PREFERRED_KEYS) : undefined;
|
|
10
|
+
return {
|
|
11
|
+
...(headerArgs ? { headerArgs } : {}),
|
|
12
|
+
collapsedBody: input.output,
|
|
13
|
+
expandedText: result,
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -10,7 +10,7 @@ export function applySkillReadDisplay(input, rendered) {
|
|
|
10
10
|
if (!skill)
|
|
11
11
|
return rendered;
|
|
12
12
|
const pathText = skill.path ?? "";
|
|
13
|
-
const expanded = expandedTextFromParts({ text: pathText
|
|
13
|
+
const expanded = expandedTextFromParts({ text: pathText }, { text: resultText(input, { empty: !pathText }) });
|
|
14
14
|
const resultStartLine = pathText ? lineCount(pathText) + 1 : 0;
|
|
15
15
|
return {
|
|
16
16
|
...rendered,
|
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import type { Theme } from "../theme.js";
|
|
2
2
|
import type { ToolBodySyntaxHighlights } from "../syntax-highlight.js";
|
|
3
|
-
export type ToolHeaderSegment = {
|
|
4
|
-
start: number;
|
|
5
|
-
end: number;
|
|
6
|
-
foreground?: string;
|
|
7
|
-
background?: string;
|
|
8
|
-
bold?: boolean;
|
|
9
|
-
strikethrough?: boolean;
|
|
10
|
-
};
|
|
11
3
|
export type ToolBodyLineStyle = {
|
|
12
4
|
startLine: number;
|
|
13
5
|
endLine?: number;
|
|
@@ -31,7 +23,6 @@ export type ToolRenderInput = {
|
|
|
31
23
|
export type ToolRenderResult = {
|
|
32
24
|
toolName?: string;
|
|
33
25
|
headerArgs?: string;
|
|
34
|
-
headerArgsSegments?: readonly ToolHeaderSegment[];
|
|
35
26
|
bodyLineStyles?: readonly ToolBodyLineStyle[];
|
|
36
27
|
bodyStyle?: "diff";
|
|
37
28
|
preserveAnsi?: boolean;
|
|
@@ -70,7 +70,7 @@ export function resultSection(input) {
|
|
|
70
70
|
export function argsAndResultExpandedText(input, argsBlock) {
|
|
71
71
|
const formattedArgs = argsBlock.trimEnd();
|
|
72
72
|
const showArgs = formattedArgs.length > 0 && formattedArgs !== "(empty)";
|
|
73
|
-
return expandedTextFromParts(...(showArgs ? [{ text: formattedArgs
|
|
73
|
+
return expandedTextFromParts(...(showArgs ? [{ text: formattedArgs }] : []), { text: resultText(input, { empty: !showArgs }) });
|
|
74
74
|
}
|
|
75
75
|
export function resultText(input, options = {}) {
|
|
76
76
|
if (input.output)
|
|
@@ -8,14 +8,24 @@
|
|
|
8
8
|
* uncommitted in pi-mono and never shipped, so every released pi-ai version
|
|
9
9
|
* (incl. 0.79.10) is affected.
|
|
10
10
|
*
|
|
11
|
-
* The fix must run at the
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* WebSocket.prototype.send once and
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
11
|
+
* The fix must run at the transport layer because the provider has TWO paths:
|
|
12
|
+
*
|
|
13
|
+
* 1. WebSocket-cached path (normal): `buildCachedWebSocketRequestBody` rebuilds
|
|
14
|
+
* the body into a DELTA after `before_provider_request`, so the hook never
|
|
15
|
+
* sees the bytes actually sent. We wrap `WebSocket.prototype.send` once and
|
|
16
|
+
* strip `content` from every non-message input item of each
|
|
17
|
+
* `response.create` frame, on the exact bytes leaving the socket.
|
|
18
|
+
*
|
|
19
|
+
* 2. SSE/fetch fallback path: once a websocket attempt fails (common at large
|
|
20
|
+
* context — full body, ~100+ input items), the transport records an SSE
|
|
21
|
+
* fallback for the session and every subsequent request goes through a plain
|
|
22
|
+
* `fetch()` POST with the FULL body (`bodyJson`). The `before_provider_request`
|
|
23
|
+
* hook should clean this, but in practice it does not reliably catch every
|
|
24
|
+
* offending item, so we also wrap `globalThis.fetch` once and strip the same
|
|
25
|
+
* spurious `content` from the request body on the exact bytes leaving the
|
|
26
|
+
* HTTP client.
|
|
27
|
+
*
|
|
28
|
+
* The `before_provider_request` hook is kept as a third secondary guard.
|
|
19
29
|
*
|
|
20
30
|
* Remove this whole module once an upstream pi-ai release carries the fix.
|
|
21
31
|
*/
|
|
@@ -31,9 +41,54 @@ type ProviderRequestContext = {
|
|
|
31
41
|
};
|
|
32
42
|
|
|
33
43
|
installWireStripper();
|
|
44
|
+
installFetchStripper();
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Strip spurious `content` from any object that carries an `input` or
|
|
48
|
+
* `messages` array. Shared core for the wire-frame, fetch, and hook paths.
|
|
49
|
+
* Returns the cleaned object plus a tally only when something changed;
|
|
50
|
+
* otherwise `undefined` (caller forwards the original untouched).
|
|
51
|
+
*/
|
|
52
|
+
export function stripCarrier(obj: unknown): { obj: Record<string, unknown>; stripped: number } | undefined {
|
|
53
|
+
if (!isRecord(obj)) return undefined;
|
|
54
|
+
const field = Array.isArray(obj.input) ? "input" : Array.isArray(obj.messages) ? "messages" : null;
|
|
55
|
+
if (!field) return undefined;
|
|
56
|
+
|
|
57
|
+
const list = obj[field] as unknown[];
|
|
58
|
+
let stripped = 0;
|
|
59
|
+
let changed = false;
|
|
60
|
+
const next = new Array(list.length);
|
|
61
|
+
for (let i = 0; i < list.length; i++) {
|
|
62
|
+
const item = list[i];
|
|
63
|
+
if (isSpuriousContentItem(item)) {
|
|
64
|
+
const { content: _drop, ...rest } = item as Record<string, unknown>;
|
|
65
|
+
next[i] = rest;
|
|
66
|
+
stripped++;
|
|
67
|
+
changed = true;
|
|
68
|
+
} else {
|
|
69
|
+
next[i] = item;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (!changed) return undefined;
|
|
73
|
+
return { obj: { ...obj, [field]: next }, stripped };
|
|
74
|
+
}
|
|
34
75
|
|
|
35
76
|
/**
|
|
36
|
-
*
|
|
77
|
+
* True only for items that carry a spurious `content`: an explicit `type` that
|
|
78
|
+
* is NOT "message". Role-based messages (no `type`) and typed messages
|
|
79
|
+
* (`type:"message"`) legitimately hold content — they are left untouched.
|
|
80
|
+
*/
|
|
81
|
+
function isSpuriousContentItem(item: unknown): boolean {
|
|
82
|
+
return (
|
|
83
|
+
isRecord(item) &&
|
|
84
|
+
typeof item.type === "string" &&
|
|
85
|
+
item.type !== "message" &&
|
|
86
|
+
Object.prototype.hasOwnProperty.call(item, "content")
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Wrap `WebSocket.prototype.send` once. For each `response.create` frame, strip
|
|
37
92
|
* the offending `content` from non-message input items and forward the cleaned
|
|
38
93
|
* frame. Forwards the original untouched on any parse/rewrite failure so the
|
|
39
94
|
* transport never breaks.
|
|
@@ -52,9 +107,11 @@ function installWireStripper(): void {
|
|
|
52
107
|
if (typeof data === "string") {
|
|
53
108
|
try {
|
|
54
109
|
const parsed = JSON.parse(data);
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
110
|
+
if (isRecord(parsed) && parsed.type === "response.create") {
|
|
111
|
+
const result = stripCarrier(parsed);
|
|
112
|
+
if (result) {
|
|
113
|
+
outgoing = JSON.stringify(result.obj);
|
|
114
|
+
}
|
|
58
115
|
}
|
|
59
116
|
} catch {
|
|
60
117
|
// Non-JSON or malformed: forward untouched.
|
|
@@ -69,43 +126,59 @@ function installWireStripper(): void {
|
|
|
69
126
|
}
|
|
70
127
|
}
|
|
71
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Wrap `globalThis.fetch` once. For JSON POST bodies carrying an `input` or
|
|
131
|
+
* `messages` array (Codex / OpenAI Responses shape), strip the spurious
|
|
132
|
+
* `content` from non-message items before the request leaves the HTTP client.
|
|
133
|
+
* This closes the SSE-fallback path that the WebSocket wrapper cannot see.
|
|
134
|
+
* Forwards the original request untouched on any non-matching/parse failure.
|
|
135
|
+
*/
|
|
136
|
+
function installFetchStripper(): void {
|
|
137
|
+
try {
|
|
138
|
+
const g = globalThis as { fetch?: typeof fetch };
|
|
139
|
+
const original = g.fetch;
|
|
140
|
+
if (typeof original !== "function") return;
|
|
141
|
+
if ((original as unknown as { __codexFixWrapped?: boolean }).__codexFixWrapped) return;
|
|
142
|
+
|
|
143
|
+
const wrapped = function fetchStripper(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
144
|
+
const nextInit = stripFetchInit(init);
|
|
145
|
+
return original.call(this, input, nextInit ?? init);
|
|
146
|
+
};
|
|
147
|
+
(wrapped as unknown as { __codexFixWrapped?: boolean }).__codexFixWrapped = true;
|
|
148
|
+
g.fetch = wrapped as typeof fetch;
|
|
149
|
+
} catch {
|
|
150
|
+
// best-effort; never break on setup
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* If `init` carries a JSON body shaped like a Responses request, return a new
|
|
156
|
+
* `init` with the stripped body. Returns `undefined` when nothing matched so
|
|
157
|
+
* the caller forwards the original `init` untouched.
|
|
158
|
+
*/
|
|
159
|
+
export function stripFetchInit(init: RequestInit | undefined): RequestInit | undefined {
|
|
160
|
+
if (!init || typeof init.body !== "string") return undefined;
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(init.body);
|
|
163
|
+
const result = stripCarrier(parsed);
|
|
164
|
+
if (!result) return undefined;
|
|
165
|
+
return { ...init, body: JSON.stringify(result.obj) };
|
|
166
|
+
} catch {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
72
171
|
/**
|
|
73
172
|
* If `frame` is a Codex `response.create` carrying an `input` (or `messages`)
|
|
74
|
-
* array, return the cleaned frame
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* returned `frame` is a clean Codex payload with no extra fields.
|
|
173
|
+
* array, return the cleaned frame plus a tally of stripped items. Returns
|
|
174
|
+
* `undefined` when nothing matched (caller forwards the original frame
|
|
175
|
+
* untouched). Exported for unit testing.
|
|
78
176
|
*/
|
|
79
177
|
export function stripContentFromWireFrame(frame: unknown): { frame: Record<string, unknown>; stripped: number } | undefined {
|
|
80
178
|
if (!isRecord(frame) || frame.type !== "response.create") return undefined;
|
|
81
|
-
const
|
|
82
|
-
if (!
|
|
83
|
-
|
|
84
|
-
const list = frame[field] as unknown[];
|
|
85
|
-
let stripped = 0;
|
|
86
|
-
let changed = false;
|
|
87
|
-
const next = new Array(list.length);
|
|
88
|
-
for (let i = 0; i < list.length; i++) {
|
|
89
|
-
const item = list[i];
|
|
90
|
-
// Only items with an explicit `type` that is NOT "message" can carry a
|
|
91
|
-
// spurious `content`. Role-based messages (no `type`) and typed
|
|
92
|
-
// messages (`type:"message"`) legitimately hold content — leave them.
|
|
93
|
-
if (
|
|
94
|
-
isRecord(item) &&
|
|
95
|
-
typeof item.type === "string" &&
|
|
96
|
-
item.type !== "message" &&
|
|
97
|
-
Object.prototype.hasOwnProperty.call(item, "content")
|
|
98
|
-
) {
|
|
99
|
-
const { content: _drop, ...rest } = item;
|
|
100
|
-
next[i] = rest;
|
|
101
|
-
stripped++;
|
|
102
|
-
changed = true;
|
|
103
|
-
} else {
|
|
104
|
-
next[i] = item;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (!changed) return undefined;
|
|
108
|
-
return { frame: { ...frame, [field]: next }, stripped };
|
|
179
|
+
const result = stripCarrier(frame);
|
|
180
|
+
if (!result) return undefined;
|
|
181
|
+
return { frame: result.obj, stripped: result.stripped };
|
|
109
182
|
}
|
|
110
183
|
|
|
111
184
|
export default function codexReasoningFix(pi: ExtensionAPI): void {
|
|
@@ -117,36 +190,12 @@ export default function codexReasoningFix(pi: ExtensionAPI): void {
|
|
|
117
190
|
|
|
118
191
|
/**
|
|
119
192
|
* Strip spurious `content` from non-message items in a full payload. Secondary
|
|
120
|
-
* guard for the SSE-fallback / non-websocket path.
|
|
121
|
-
*
|
|
193
|
+
* guard for the SSE-fallback / non-websocket path. Returns the same reference
|
|
194
|
+
* when nothing changed; exported for unit testing.
|
|
122
195
|
*/
|
|
123
196
|
export function stripReasoningContentFromPayload(payload: unknown): unknown {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const messagesList = Array.isArray(payload.messages) ? payload.messages : undefined;
|
|
127
|
-
const field = inputList ? "input" : messagesList ? "messages" : undefined;
|
|
128
|
-
const list = inputList ?? messagesList;
|
|
129
|
-
if (!field || !list) return payload;
|
|
130
|
-
|
|
131
|
-
let changed = false;
|
|
132
|
-
const next = new Array(list.length);
|
|
133
|
-
for (let i = 0; i < list.length; i++) {
|
|
134
|
-
const item = list[i];
|
|
135
|
-
if (
|
|
136
|
-
isRecord(item) &&
|
|
137
|
-
typeof item.type === "string" &&
|
|
138
|
-
item.type !== "message" &&
|
|
139
|
-
Object.prototype.hasOwnProperty.call(item, "content")
|
|
140
|
-
) {
|
|
141
|
-
const { content: _drop, ...rest } = item;
|
|
142
|
-
next[i] = rest;
|
|
143
|
-
changed = true;
|
|
144
|
-
} else {
|
|
145
|
-
next[i] = item;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (!changed) return payload;
|
|
149
|
-
return { ...payload, [field]: next };
|
|
197
|
+
const result = stripCarrier(payload);
|
|
198
|
+
return result ? result.obj : payload;
|
|
150
199
|
}
|
|
151
200
|
|
|
152
201
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -532,6 +532,23 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
532
532
|
}, ctx)
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
const hasCompressionSuggestion =
|
|
536
|
+
candidate !== null || messageCandidates.length > 0
|
|
537
|
+
if (!manualEmergencyOnly && !hasCompressionSuggestion) {
|
|
538
|
+
const clearedAnchors = clearDcpNudgeAnchors(state)
|
|
539
|
+
state.consecutiveIgnoredStrongNudges = 0
|
|
540
|
+
if (clearedAnchors > 0) await saveDcpState(ctx, state)
|
|
541
|
+
if (nudgeType || clearedAnchors > 0) {
|
|
542
|
+
writeDcpDebugLog(effectiveConfig, "context.no_compression_candidate", {
|
|
543
|
+
contextPercent,
|
|
544
|
+
thresholds,
|
|
545
|
+
nudgeType,
|
|
546
|
+
clearedAnchors,
|
|
547
|
+
state: summarizeDcpState(state),
|
|
548
|
+
}, ctx)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
535
552
|
// Track consecutive ignored context-strong nudges for the
|
|
536
553
|
// auto-compress fallback. A strong nudge is "ignored" if it fires
|
|
537
554
|
// again on a later context event without a successful compress in
|
|
@@ -539,9 +556,12 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
539
556
|
// the emergency threshold (handled by the below-threshold early
|
|
540
557
|
// return above, which clears anchors; counter is also reset on any
|
|
541
558
|
// successful compress in the compress tool).
|
|
542
|
-
if (
|
|
559
|
+
if (
|
|
560
|
+
hasCompressionSuggestion &&
|
|
561
|
+
(nudgeType === "context-strong" || nudgeType === "context-soft")
|
|
562
|
+
) {
|
|
543
563
|
state.consecutiveIgnoredStrongNudges += 1
|
|
544
|
-
} else if (nudgeType === null) {
|
|
564
|
+
} else if (nudgeType === null || !hasCompressionSuggestion) {
|
|
545
565
|
state.consecutiveIgnoredStrongNudges = 0
|
|
546
566
|
}
|
|
547
567
|
|
|
@@ -604,7 +624,7 @@ export default async function dcpModule(pi: ExtensionAPI): Promise<void> {
|
|
|
604
624
|
}
|
|
605
625
|
}
|
|
606
626
|
|
|
607
|
-
if (nudgeType && !manualEmergencyOnly) {
|
|
627
|
+
if (nudgeType && !manualEmergencyOnly && hasCompressionSuggestion) {
|
|
608
628
|
const nudgeText = appendConcreteNudgeGuidance(
|
|
609
629
|
baseNudgeText(nudgeType),
|
|
610
630
|
candidate,
|