@oh-my-pi/pi-coding-agent 13.5.6 → 13.5.7
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/package.json +7 -7
- package/src/extensibility/extensions/loader.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -3
- package/src/extensibility/hooks/loader.ts +1 -1
- package/src/extensibility/hooks/types.ts +3 -2
- package/src/modes/components/bash-execution.ts +30 -16
- package/src/modes/components/tool-execution.ts +2 -1
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/sdk.ts +1 -0
- package/src/session/agent-session.ts +25 -6
- package/src/session/compaction/branch-summarization.ts +8 -1
- package/src/session/compaction/compaction.ts +8 -1
- package/src/session/messages.ts +19 -2
- package/src/session/session-manager.ts +15 -3
- package/src/session/streaming-output.ts +2 -1
- package/src/tools/bash-interactive.ts +2 -1
- package/src/tools/bash.ts +13 -5
- package/src/tui/output-block.ts +9 -2
- package/src/utils/sixel.ts +69 -0
- package/src/web/search/providers/anthropic.ts +1 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.5.
|
|
4
|
+
"version": "13.5.7",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.5.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.5.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.5.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.5.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.5.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.5.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.5.7",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.5.7",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.5.7",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.5.7",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.5.7",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.5.7",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -174,7 +174,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
sendMessage<T = unknown>(
|
|
177
|
-
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
|
177
|
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
178
178
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
179
179
|
): void {
|
|
180
180
|
this.runtime.sendMessage(message, options);
|
|
@@ -824,7 +824,7 @@ export interface ToolResultEventResult {
|
|
|
824
824
|
}
|
|
825
825
|
|
|
826
826
|
export interface BeforeAgentStartEventResult {
|
|
827
|
-
message?: Pick<CustomMessage, "customType" | "content" | "display" | "details">;
|
|
827
|
+
message?: Pick<CustomMessage, "customType" | "content" | "display" | "details" | "attribution">;
|
|
828
828
|
/** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */
|
|
829
829
|
systemPrompt?: string;
|
|
830
830
|
}
|
|
@@ -1015,7 +1015,7 @@ export interface ExtensionAPI {
|
|
|
1015
1015
|
|
|
1016
1016
|
/** Send a custom message to the session. */
|
|
1017
1017
|
sendMessage<T = unknown>(
|
|
1018
|
-
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
|
1018
|
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
1019
1019
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
1020
1020
|
): void;
|
|
1021
1021
|
|
|
@@ -1184,7 +1184,7 @@ export interface ExtensionShortcut {
|
|
|
1184
1184
|
type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
|
1185
1185
|
|
|
1186
1186
|
export type SendMessageHandler = <T = unknown>(
|
|
1187
|
-
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
|
1187
|
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
1188
1188
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
1189
1189
|
) => void;
|
|
1190
1190
|
|
|
@@ -23,7 +23,7 @@ type HandlerFn = (...args: unknown[]) => Promise<unknown>;
|
|
|
23
23
|
* Send message handler type for pi.sendMessage().
|
|
24
24
|
*/
|
|
25
25
|
export type SendMessageHandler = <T = unknown>(
|
|
26
|
-
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details">,
|
|
26
|
+
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
27
27
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
|
|
28
28
|
) => void;
|
|
29
29
|
|
|
@@ -585,7 +585,7 @@ export interface ToolResultEventResult {
|
|
|
585
585
|
*/
|
|
586
586
|
export interface BeforeAgentStartEventResult {
|
|
587
587
|
/** Message to inject into context (persisted to session, visible in TUI) */
|
|
588
|
-
message?: Pick<HookMessage, "customType" | "content" | "display" | "details">;
|
|
588
|
+
message?: Pick<HookMessage, "customType" | "content" | "display" | "details" | "attribution">;
|
|
589
589
|
}
|
|
590
590
|
|
|
591
591
|
/** Return type for session_before_switch handlers */
|
|
@@ -733,12 +733,13 @@ export interface HookAPI {
|
|
|
733
733
|
* @param message.content - Message content (string or TextContent/ImageContent array)
|
|
734
734
|
* @param message.display - Whether to show in TUI (true = styled display, false = hidden)
|
|
735
735
|
* @param message.details - Optional hook-specific metadata (not sent to LLM)
|
|
736
|
+
* @param message.attribution - Who initiated the message for billing/attribution semantics ("user" | "agent")
|
|
736
737
|
* @param options.triggerTurn - If true and agent is idle, triggers a new LLM turn. Default: false.
|
|
737
738
|
* If agent is streaming, message is queued and triggerTurn is ignored.
|
|
738
739
|
* @param options.deliverAs - How to deliver the message: "steer" or "followUp".
|
|
739
740
|
*/
|
|
740
741
|
sendMessage<T = unknown>(
|
|
741
|
-
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details">,
|
|
742
|
+
message: Pick<HookMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
742
743
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" },
|
|
743
744
|
): void;
|
|
744
745
|
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
6
|
-
import { Container, Loader, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
6
|
+
import { Container, ImageProtocol, Loader, Spacer, TERMINAL, Text, type TUI } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import { getSymbolTheme, theme } from "../../modes/theme/theme";
|
|
8
8
|
import { formatTruncationMetaNotice, type TruncationMeta } from "../../tools/output-meta";
|
|
9
|
+
import { getSixelLineMask, sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
9
10
|
import { DynamicBorder } from "./dynamic-border";
|
|
10
11
|
import { truncateToVisualLines } from "./visual-truncate";
|
|
11
12
|
|
|
@@ -75,18 +76,18 @@ export class BashExecutionComponent extends Container {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
appendOutput(chunk: string): void {
|
|
78
|
-
const clean =
|
|
79
|
+
const clean = sanitizeWithOptionalSixelPassthrough(chunk, sanitizeText);
|
|
79
80
|
|
|
80
81
|
// Append to output lines
|
|
81
|
-
const
|
|
82
|
-
if (this.#outputLines.length > 0 &&
|
|
83
|
-
|
|
84
|
-
this.#outputLines[
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
this.#outputLines.push(...
|
|
82
|
+
const incomingLines = clean.split("\n");
|
|
83
|
+
if (this.#outputLines.length > 0 && incomingLines.length > 0) {
|
|
84
|
+
const lastIndex = this.#outputLines.length - 1;
|
|
85
|
+
const mergedLines = [`${this.#outputLines[lastIndex]}${incomingLines[0]}`, ...incomingLines.slice(1)];
|
|
86
|
+
const clampedMergedLines = this.#clampLinesPreservingSixel(mergedLines);
|
|
87
|
+
this.#outputLines[lastIndex] = clampedMergedLines[0] ?? "";
|
|
88
|
+
this.#outputLines.push(...clampedMergedLines.slice(1));
|
|
88
89
|
} else {
|
|
89
|
-
this.#outputLines.push(...
|
|
90
|
+
this.#outputLines.push(...this.#clampLinesPreservingSixel(incomingLines));
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
this.#updateDisplay();
|
|
@@ -120,6 +121,9 @@ export class BashExecutionComponent extends Container {
|
|
|
120
121
|
// Apply preview truncation based on expanded state
|
|
121
122
|
const previewLogicalLines = availableLines.slice(-PREVIEW_LINES);
|
|
122
123
|
const hiddenLineCount = availableLines.length - previewLogicalLines.length;
|
|
124
|
+
const sixelLineMask =
|
|
125
|
+
TERMINAL.imageProtocol === ImageProtocol.Sixel ? getSixelLineMask(availableLines) : undefined;
|
|
126
|
+
const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
|
|
123
127
|
|
|
124
128
|
// Rebuild content container
|
|
125
129
|
this.#contentContainer.clear();
|
|
@@ -130,9 +134,10 @@ export class BashExecutionComponent extends Container {
|
|
|
130
134
|
|
|
131
135
|
// Output
|
|
132
136
|
if (availableLines.length > 0) {
|
|
133
|
-
if (this.#expanded) {
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
if (this.#expanded || hasSixelOutput) {
|
|
138
|
+
const displayText = availableLines
|
|
139
|
+
.map((line, index) => (sixelLineMask?.[index] ? line : theme.fg("muted", line)))
|
|
140
|
+
.join("\n");
|
|
136
141
|
this.#contentContainer.addChild(new Text(`\n${displayText}`, 1, 0));
|
|
137
142
|
} else {
|
|
138
143
|
// Use shared visual truncation utility, recomputed per render width
|
|
@@ -155,7 +160,7 @@ export class BashExecutionComponent extends Container {
|
|
|
155
160
|
const statusParts: string[] = [];
|
|
156
161
|
|
|
157
162
|
// Show how many lines are hidden (collapsed preview)
|
|
158
|
-
if (hiddenLineCount > 0) {
|
|
163
|
+
if (hiddenLineCount > 0 && !hasSixelOutput) {
|
|
159
164
|
statusParts.push(theme.fg("dim", `… ${hiddenLineCount} more lines (ctrl+o to expand)`));
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -183,9 +188,18 @@ export class BashExecutionComponent extends Container {
|
|
|
183
188
|
return `${line.slice(0, MAX_DISPLAY_LINE_CHARS)}… [${omitted} chars omitted]`;
|
|
184
189
|
}
|
|
185
190
|
|
|
191
|
+
#clampLinesPreservingSixel(lines: string[]): string[] {
|
|
192
|
+
if (lines.length === 0) return [];
|
|
193
|
+
const sixelLineMask = getSixelLineMask(lines);
|
|
194
|
+
if (!sixelLineMask.some(Boolean)) {
|
|
195
|
+
return lines.map(line => this.#clampDisplayLine(line));
|
|
196
|
+
}
|
|
197
|
+
return lines.map((line, index) => (sixelLineMask[index] ? line : this.#clampDisplayLine(line)));
|
|
198
|
+
}
|
|
199
|
+
|
|
186
200
|
#setOutput(output: string): void {
|
|
187
|
-
const clean =
|
|
188
|
-
this.#outputLines = clean ? clean.split("\n")
|
|
201
|
+
const clean = sanitizeWithOptionalSixelPassthrough(output, sanitizeText);
|
|
202
|
+
this.#outputLines = clean ? this.#clampLinesPreservingSixel(clean.split("\n")) : [];
|
|
189
203
|
}
|
|
190
204
|
|
|
191
205
|
/**
|
|
@@ -34,6 +34,7 @@ import { formatExpandHint, truncateToWidth } from "../../tools/render-utils";
|
|
|
34
34
|
import { toolRenderers } from "../../tools/renderers";
|
|
35
35
|
import { renderStatusLine } from "../../tui";
|
|
36
36
|
import { convertToPng } from "../../utils/image-convert";
|
|
37
|
+
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
|
|
37
38
|
import { renderDiff } from "./diff";
|
|
38
39
|
|
|
39
40
|
function ensureInvalidate(component: unknown): Component {
|
|
@@ -589,7 +590,7 @@ export class ToolExecutionComponent extends Container {
|
|
|
589
590
|
|
|
590
591
|
let output = textBlocks
|
|
591
592
|
.map((c: any) => {
|
|
592
|
-
return
|
|
593
|
+
return sanitizeWithOptionalSixelPassthrough(c.text || "", sanitizeText);
|
|
593
594
|
})
|
|
594
595
|
.join("\n");
|
|
595
596
|
|
package/src/sdk.ts
CHANGED
|
@@ -422,6 +422,7 @@ export class AgentSession {
|
|
|
422
422
|
content: reminderText,
|
|
423
423
|
display: false,
|
|
424
424
|
details: { toolName: action.sourceToolName },
|
|
425
|
+
attribution: "agent",
|
|
425
426
|
timestamp: Date.now(),
|
|
426
427
|
});
|
|
427
428
|
});
|
|
@@ -597,6 +598,7 @@ export class AgentSession {
|
|
|
597
598
|
content: injection.content,
|
|
598
599
|
display: false,
|
|
599
600
|
details,
|
|
601
|
+
attribution: "agent",
|
|
600
602
|
timestamp: Date.now(),
|
|
601
603
|
});
|
|
602
604
|
this.sessionManager.appendCustomMessageEntry(
|
|
@@ -604,6 +606,7 @@ export class AgentSession {
|
|
|
604
606
|
injection.content,
|
|
605
607
|
false,
|
|
606
608
|
details,
|
|
609
|
+
"agent",
|
|
607
610
|
);
|
|
608
611
|
this.#markTtsrInjected(details.rules);
|
|
609
612
|
}
|
|
@@ -642,6 +645,7 @@ export class AgentSession {
|
|
|
642
645
|
event.message.content,
|
|
643
646
|
event.message.display,
|
|
644
647
|
event.message.details,
|
|
648
|
+
event.message.attribution ?? "agent",
|
|
645
649
|
);
|
|
646
650
|
if (event.message.role === "custom" && event.message.customType === "ttsr-injection") {
|
|
647
651
|
this.#markTtsrInjected(this.#extractTtsrRuleNames(event.message.details));
|
|
@@ -1011,6 +1015,7 @@ export class AgentSession {
|
|
|
1011
1015
|
content: injection.content,
|
|
1012
1016
|
display: false,
|
|
1013
1017
|
details: { rules: injection.rules.map(rule => rule.name) },
|
|
1018
|
+
attribution: "agent",
|
|
1014
1019
|
timestamp: Date.now(),
|
|
1015
1020
|
});
|
|
1016
1021
|
this.#ensureTtsrResumePromise();
|
|
@@ -1809,6 +1814,7 @@ export class AgentSession {
|
|
|
1809
1814
|
customType: "plan-mode-reference",
|
|
1810
1815
|
content,
|
|
1811
1816
|
display: false,
|
|
1817
|
+
attribution: "agent",
|
|
1812
1818
|
timestamp: Date.now(),
|
|
1813
1819
|
};
|
|
1814
1820
|
}
|
|
@@ -1849,6 +1855,7 @@ export class AgentSession {
|
|
|
1849
1855
|
customType: "plan-mode-context",
|
|
1850
1856
|
content,
|
|
1851
1857
|
display: false,
|
|
1858
|
+
attribution: "agent",
|
|
1852
1859
|
timestamp: Date.now(),
|
|
1853
1860
|
};
|
|
1854
1861
|
}
|
|
@@ -1910,8 +1917,8 @@ export class AgentSession {
|
|
|
1910
1917
|
}
|
|
1911
1918
|
|
|
1912
1919
|
const message = options?.synthetic
|
|
1913
|
-
? { role: "developer" as const, content: userContent, timestamp: Date.now() }
|
|
1914
|
-
: { role: "user" as const, content: userContent, timestamp: Date.now() };
|
|
1920
|
+
? { role: "developer" as const, content: userContent, attribution: "agent" as const, timestamp: Date.now() }
|
|
1921
|
+
: { role: "user" as const, content: userContent, attribution: "user" as const, timestamp: Date.now() };
|
|
1915
1922
|
|
|
1916
1923
|
await this.#promptWithMessage(message, expandedText, options);
|
|
1917
1924
|
if (!options?.synthetic) {
|
|
@@ -1920,7 +1927,7 @@ export class AgentSession {
|
|
|
1920
1927
|
}
|
|
1921
1928
|
|
|
1922
1929
|
async promptCustomMessage<T = unknown>(
|
|
1923
|
-
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
|
1930
|
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
1924
1931
|
options?: Pick<PromptOptions, "streamingBehavior" | "toolChoice">,
|
|
1925
1932
|
): Promise<void> {
|
|
1926
1933
|
const textContent =
|
|
@@ -1945,6 +1952,7 @@ export class AgentSession {
|
|
|
1945
1952
|
content: message.content,
|
|
1946
1953
|
display: message.display,
|
|
1947
1954
|
details: message.details,
|
|
1955
|
+
attribution: message.attribution ?? "agent",
|
|
1948
1956
|
timestamp: Date.now(),
|
|
1949
1957
|
};
|
|
1950
1958
|
|
|
@@ -2033,6 +2041,8 @@ export class AgentSession {
|
|
|
2033
2041
|
this.#baseSystemPrompt,
|
|
2034
2042
|
);
|
|
2035
2043
|
if (result?.messages) {
|
|
2044
|
+
const promptAttribution: "user" | "agent" | undefined =
|
|
2045
|
+
"attribution" in message ? message.attribution : undefined;
|
|
2036
2046
|
for (const msg of result.messages) {
|
|
2037
2047
|
messages.push({
|
|
2038
2048
|
role: "custom",
|
|
@@ -2040,6 +2050,7 @@ export class AgentSession {
|
|
|
2040
2050
|
content: msg.content,
|
|
2041
2051
|
display: msg.display,
|
|
2042
2052
|
details: msg.details,
|
|
2053
|
+
attribution: msg.attribution ?? promptAttribution ?? (message.role === "user" ? "user" : "agent"),
|
|
2043
2054
|
timestamp: Date.now(),
|
|
2044
2055
|
});
|
|
2045
2056
|
}
|
|
@@ -2239,6 +2250,7 @@ export class AgentSession {
|
|
|
2239
2250
|
this.agent.steer({
|
|
2240
2251
|
role: "user",
|
|
2241
2252
|
content,
|
|
2253
|
+
attribution: "user",
|
|
2242
2254
|
timestamp: Date.now(),
|
|
2243
2255
|
});
|
|
2244
2256
|
}
|
|
@@ -2256,6 +2268,7 @@ export class AgentSession {
|
|
|
2256
2268
|
this.agent.followUp({
|
|
2257
2269
|
role: "user",
|
|
2258
2270
|
content,
|
|
2271
|
+
attribution: "user",
|
|
2259
2272
|
timestamp: Date.now(),
|
|
2260
2273
|
});
|
|
2261
2274
|
}
|
|
@@ -2286,7 +2299,7 @@ export class AgentSession {
|
|
|
2286
2299
|
* - Not streaming + no trigger: appends to state/session, no turn
|
|
2287
2300
|
*/
|
|
2288
2301
|
async sendCustomMessage<T = unknown>(
|
|
2289
|
-
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details">,
|
|
2302
|
+
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
2290
2303
|
options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
2291
2304
|
): Promise<void> {
|
|
2292
2305
|
const appMessage: CustomMessage<T> = {
|
|
@@ -2295,6 +2308,7 @@ export class AgentSession {
|
|
|
2295
2308
|
content: message.content,
|
|
2296
2309
|
display: message.display,
|
|
2297
2310
|
details: message.details,
|
|
2311
|
+
attribution: message.attribution ?? "agent",
|
|
2298
2312
|
timestamp: Date.now(),
|
|
2299
2313
|
};
|
|
2300
2314
|
if (this.isStreaming) {
|
|
@@ -2322,6 +2336,7 @@ export class AgentSession {
|
|
|
2322
2336
|
message.content,
|
|
2323
2337
|
message.display,
|
|
2324
2338
|
message.details,
|
|
2339
|
+
message.attribution ?? "agent",
|
|
2325
2340
|
);
|
|
2326
2341
|
}
|
|
2327
2342
|
|
|
@@ -3212,7 +3227,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3212
3227
|
|
|
3213
3228
|
// Inject the handoff document as a custom message
|
|
3214
3229
|
const handoffContent = `<handoff-context>\n${handoffText}\n</handoff-context>\n\nThe above is a handoff document from a previous session. Use this context to continue the work seamlessly.`;
|
|
3215
|
-
this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true);
|
|
3230
|
+
this.sessionManager.appendCustomMessageEntry("handoff", handoffContent, true, undefined, "agent");
|
|
3216
3231
|
|
|
3217
3232
|
// Rebuild agent messages from session
|
|
3218
3233
|
const sessionContext = this.sessionManager.buildSessionContext();
|
|
@@ -3309,6 +3324,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3309
3324
|
this.agent.appendMessage({
|
|
3310
3325
|
role: "developer",
|
|
3311
3326
|
content: [{ type: "text", text: reminder }],
|
|
3327
|
+
attribution: "agent",
|
|
3312
3328
|
timestamp: Date.now(),
|
|
3313
3329
|
});
|
|
3314
3330
|
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
@@ -3339,9 +3355,10 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3339
3355
|
content: report,
|
|
3340
3356
|
display: false,
|
|
3341
3357
|
details,
|
|
3358
|
+
attribution: "agent",
|
|
3342
3359
|
timestamp: Date.now(),
|
|
3343
3360
|
});
|
|
3344
|
-
this.sessionManager.appendCustomMessageEntry("rewind-report", report, false, details);
|
|
3361
|
+
this.sessionManager.appendCustomMessageEntry("rewind-report", report, false, details, "agent");
|
|
3345
3362
|
this.#checkpointState = undefined;
|
|
3346
3363
|
this.#pendingRewindReport = undefined;
|
|
3347
3364
|
}
|
|
@@ -3460,6 +3477,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3460
3477
|
this.agent.appendMessage({
|
|
3461
3478
|
role: "developer",
|
|
3462
3479
|
content: [{ type: "text", text: reminder }],
|
|
3480
|
+
attribution: "agent",
|
|
3463
3481
|
timestamp: Date.now(),
|
|
3464
3482
|
});
|
|
3465
3483
|
this.#scheduleAgentContinue({ generation: this.#promptGeneration });
|
|
@@ -3876,6 +3894,7 @@ Be thorough - include exact file paths, function names, error messages, and tech
|
|
|
3876
3894
|
{
|
|
3877
3895
|
role: "developer",
|
|
3878
3896
|
content: [{ type: "text", text: "Continue if you have next steps." }],
|
|
3897
|
+
attribution: "agent",
|
|
3879
3898
|
timestamp: Date.now(),
|
|
3880
3899
|
},
|
|
3881
3900
|
"Continue if you have next steps.",
|
|
@@ -149,7 +149,14 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
|
|
149
149
|
return entry.message;
|
|
150
150
|
|
|
151
151
|
case "custom_message":
|
|
152
|
-
return createCustomMessage(
|
|
152
|
+
return createCustomMessage(
|
|
153
|
+
entry.customType,
|
|
154
|
+
entry.content,
|
|
155
|
+
entry.display,
|
|
156
|
+
entry.details,
|
|
157
|
+
entry.timestamp,
|
|
158
|
+
entry.attribution,
|
|
159
|
+
);
|
|
153
160
|
|
|
154
161
|
case "branch_summary":
|
|
155
162
|
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
|
@@ -81,7 +81,14 @@ function getMessageFromEntry(entry: SessionEntry): AgentMessage | undefined {
|
|
|
81
81
|
return entry.message;
|
|
82
82
|
}
|
|
83
83
|
if (entry.type === "custom_message") {
|
|
84
|
-
return createCustomMessage(
|
|
84
|
+
return createCustomMessage(
|
|
85
|
+
entry.customType,
|
|
86
|
+
entry.content,
|
|
87
|
+
entry.display,
|
|
88
|
+
entry.details,
|
|
89
|
+
entry.timestamp,
|
|
90
|
+
entry.attribution,
|
|
91
|
+
);
|
|
85
92
|
}
|
|
86
93
|
if (entry.type === "branch_summary") {
|
|
87
94
|
return createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp);
|
package/src/session/messages.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and provides a transformer to convert them to LLM-compatible messages.
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
8
|
-
import type { ImageContent, Message, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
8
|
+
import type { ImageContent, Message, MessageAttribution, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
|
|
9
9
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
10
10
|
import branchSummaryContextPrompt from "../prompts/compaction/branch-summary-context.md" with { type: "text" };
|
|
11
11
|
import compactionSummaryContextPrompt from "../prompts/compaction/compaction-summary-context.md" with { type: "text" };
|
|
@@ -75,6 +75,8 @@ export interface CustomMessage<T = unknown> {
|
|
|
75
75
|
content: string | (TextContent | ImageContent)[];
|
|
76
76
|
display: boolean;
|
|
77
77
|
details?: T;
|
|
78
|
+
/** Who initiated this message for billing/attribution semantics. */
|
|
79
|
+
attribution?: MessageAttribution;
|
|
78
80
|
timestamp: number;
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -87,6 +89,8 @@ export interface HookMessage<T = unknown> {
|
|
|
87
89
|
content: string | (TextContent | ImageContent)[];
|
|
88
90
|
display: boolean;
|
|
89
91
|
details?: T;
|
|
92
|
+
/** Who initiated this message for billing/attribution semantics. */
|
|
93
|
+
attribution?: MessageAttribution;
|
|
90
94
|
timestamp: number;
|
|
91
95
|
}
|
|
92
96
|
|
|
@@ -206,6 +210,7 @@ export function createCustomMessage(
|
|
|
206
210
|
display: boolean,
|
|
207
211
|
details: unknown | undefined,
|
|
208
212
|
timestamp: string,
|
|
213
|
+
attribution?: MessageAttribution,
|
|
209
214
|
): CustomMessage {
|
|
210
215
|
return {
|
|
211
216
|
role: "custom",
|
|
@@ -213,6 +218,7 @@ export function createCustomMessage(
|
|
|
213
218
|
content,
|
|
214
219
|
display,
|
|
215
220
|
details,
|
|
221
|
+
attribution,
|
|
216
222
|
timestamp: new Date(timestamp).getTime(),
|
|
217
223
|
};
|
|
218
224
|
}
|
|
@@ -236,6 +242,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
|
236
242
|
return {
|
|
237
243
|
role: "user",
|
|
238
244
|
content: [{ type: "text", text: bashExecutionToText(m) }],
|
|
245
|
+
attribution: "user",
|
|
239
246
|
timestamp: m.timestamp,
|
|
240
247
|
};
|
|
241
248
|
case "pythonExecution":
|
|
@@ -245,14 +252,18 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
|
245
252
|
return {
|
|
246
253
|
role: "user",
|
|
247
254
|
content: [{ type: "text", text: pythonExecutionToText(m) }],
|
|
255
|
+
attribution: "user",
|
|
248
256
|
timestamp: m.timestamp,
|
|
249
257
|
};
|
|
250
258
|
case "custom":
|
|
251
259
|
case "hookMessage": {
|
|
252
260
|
const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content;
|
|
261
|
+
const role = "user";
|
|
262
|
+
const attribution = m.attribution;
|
|
253
263
|
return {
|
|
254
|
-
role
|
|
264
|
+
role,
|
|
255
265
|
content,
|
|
266
|
+
attribution,
|
|
256
267
|
timestamp: m.timestamp,
|
|
257
268
|
};
|
|
258
269
|
}
|
|
@@ -265,6 +276,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
|
265
276
|
text: renderPromptTemplate(BRANCH_SUMMARY_TEMPLATE, { summary: m.summary }),
|
|
266
277
|
},
|
|
267
278
|
],
|
|
279
|
+
attribution: "agent",
|
|
268
280
|
timestamp: m.timestamp,
|
|
269
281
|
};
|
|
270
282
|
case "compactionSummary":
|
|
@@ -276,6 +288,7 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
|
276
288
|
text: renderPromptTemplate(COMPACTION_SUMMARY_TEMPLATE, { summary: m.summary }),
|
|
277
289
|
},
|
|
278
290
|
],
|
|
291
|
+
attribution: "agent",
|
|
279
292
|
timestamp: m.timestamp,
|
|
280
293
|
};
|
|
281
294
|
case "fileMention": {
|
|
@@ -296,17 +309,21 @@ export function convertToLlm(messages: AgentMessage[]): Message[] {
|
|
|
296
309
|
return {
|
|
297
310
|
role: "user",
|
|
298
311
|
content,
|
|
312
|
+
attribution: "user",
|
|
299
313
|
timestamp: m.timestamp,
|
|
300
314
|
};
|
|
301
315
|
}
|
|
302
316
|
case "user":
|
|
317
|
+
return { ...m, attribution: m.attribution ?? "user" };
|
|
303
318
|
case "developer":
|
|
319
|
+
return { ...m, attribution: m.attribution ?? "agent" };
|
|
304
320
|
case "assistant":
|
|
305
321
|
return m;
|
|
306
322
|
case "toolResult":
|
|
307
323
|
return {
|
|
308
324
|
...m,
|
|
309
325
|
content: getPrunedToolResultContent(m as ToolResultMessage),
|
|
326
|
+
attribution: m.attribution ?? "agent",
|
|
310
327
|
};
|
|
311
328
|
default:
|
|
312
329
|
// biome-ignore lint/correctness/noSwitchDeclarations: fine
|
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
5
|
-
import type { ImageContent, Message, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
5
|
+
import type { ImageContent, Message, MessageAttribution, TextContent, Usage } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import { getTerminalId } from "@oh-my-pi/pi-tui";
|
|
7
7
|
import {
|
|
8
8
|
getBlobsDir,
|
|
@@ -151,7 +151,7 @@ export interface ModeChangeEntry extends SessionEntryBase {
|
|
|
151
151
|
* Use customType to identify your extension's entries.
|
|
152
152
|
*
|
|
153
153
|
* Unlike CustomEntry, this DOES participate in LLM context.
|
|
154
|
-
* The content
|
|
154
|
+
* The content participates in LLM context through convertToLlm().
|
|
155
155
|
* Use details for extension-specific metadata (not sent to LLM).
|
|
156
156
|
*
|
|
157
157
|
* display controls TUI rendering:
|
|
@@ -164,6 +164,8 @@ export interface CustomMessageEntry<T = unknown> extends SessionEntryBase {
|
|
|
164
164
|
content: string | (TextContent | ImageContent)[];
|
|
165
165
|
details?: T;
|
|
166
166
|
display: boolean;
|
|
167
|
+
/** Who initiated this message for billing/attribution semantics. */
|
|
168
|
+
attribution?: MessageAttribution;
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
/** Session entry - has id/parentId for tree structure (returned by "read" methods in SessionManager) */
|
|
@@ -483,7 +485,14 @@ export function buildSessionContext(
|
|
|
483
485
|
messages.push(entry.message);
|
|
484
486
|
} else if (entry.type === "custom_message") {
|
|
485
487
|
messages.push(
|
|
486
|
-
createCustomMessage(
|
|
488
|
+
createCustomMessage(
|
|
489
|
+
entry.customType,
|
|
490
|
+
entry.content,
|
|
491
|
+
entry.display,
|
|
492
|
+
entry.details,
|
|
493
|
+
entry.timestamp,
|
|
494
|
+
entry.attribution,
|
|
495
|
+
),
|
|
487
496
|
);
|
|
488
497
|
} else if (entry.type === "branch_summary" && entry.summary) {
|
|
489
498
|
messages.push(createBranchSummaryMessage(entry.summary, entry.fromId, entry.timestamp));
|
|
@@ -1825,6 +1834,7 @@ export class SessionManager {
|
|
|
1825
1834
|
* @param content Message content (string or TextContent/ImageContent array)
|
|
1826
1835
|
* @param display Whether to show in TUI (true = styled display, false = hidden)
|
|
1827
1836
|
* @param details Optional extension-specific metadata (not sent to LLM)
|
|
1837
|
+
* @param attribution Who initiated this message for billing/attribution semantics
|
|
1828
1838
|
* @returns Entry id
|
|
1829
1839
|
*/
|
|
1830
1840
|
appendCustomMessageEntry<T = unknown>(
|
|
@@ -1832,6 +1842,7 @@ export class SessionManager {
|
|
|
1832
1842
|
content: string | (TextContent | ImageContent)[],
|
|
1833
1843
|
display: boolean,
|
|
1834
1844
|
details?: T,
|
|
1845
|
+
attribution: MessageAttribution = "agent",
|
|
1835
1846
|
): string {
|
|
1836
1847
|
const entry: CustomMessageEntry<T> = {
|
|
1837
1848
|
type: "custom_message",
|
|
@@ -1839,6 +1850,7 @@ export class SessionManager {
|
|
|
1839
1850
|
content,
|
|
1840
1851
|
display,
|
|
1841
1852
|
details,
|
|
1853
|
+
attribution,
|
|
1842
1854
|
id: generateId(this.#byId),
|
|
1843
1855
|
parentId: this.#leafId,
|
|
1844
1856
|
timestamp: new Date().toISOString(),
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sanitizeText } from "@oh-my-pi/pi-natives";
|
|
2
2
|
import { formatBytes } from "../tools/render-utils";
|
|
3
|
+
import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
|
3
4
|
|
|
4
5
|
// =============================================================================
|
|
5
6
|
// Constants
|
|
@@ -571,7 +572,7 @@ export class OutputSink {
|
|
|
571
572
|
}
|
|
572
573
|
|
|
573
574
|
async push(chunk: string): Promise<void> {
|
|
574
|
-
chunk =
|
|
575
|
+
chunk = sanitizeWithOptionalSixelPassthrough(chunk, sanitizeText);
|
|
575
576
|
this.#onChunk?.(chunk);
|
|
576
577
|
|
|
577
578
|
const dataBytes = Buffer.byteLength(chunk, "utf-8");
|
|
@@ -14,6 +14,7 @@ import xterm from "@xterm/headless";
|
|
|
14
14
|
import { NON_INTERACTIVE_ENV } from "../exec/non-interactive-env";
|
|
15
15
|
import type { Theme } from "../modes/theme/theme";
|
|
16
16
|
import { OutputSink, type OutputSummary } from "../session/streaming-output";
|
|
17
|
+
import { sanitizeWithOptionalSixelPassthrough } from "../utils/sixel";
|
|
17
18
|
import { formatStatusIcon, replaceTabs } from "./render-utils";
|
|
18
19
|
|
|
19
20
|
export interface BashInteractiveResult extends OutputSummary {
|
|
@@ -24,7 +25,7 @@ export interface BashInteractiveResult extends OutputSummary {
|
|
|
24
25
|
|
|
25
26
|
function normalizeCaptureChunk(chunk: string): string {
|
|
26
27
|
const normalized = chunk.replace(/\r\n/gu, "\n").replace(/\r/gu, "\n");
|
|
27
|
-
return
|
|
28
|
+
return sanitizeWithOptionalSixelPassthrough(normalized, sanitizeText);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const XtermTerminal = xterm.Terminal;
|
package/src/tools/bash.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as fs from "node:fs";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
4
4
|
import type { Component } from "@oh-my-pi/pi-tui";
|
|
5
|
-
import { Text } from "@oh-my-pi/pi-tui";
|
|
5
|
+
import { ImageProtocol, TERMINAL, Text } from "@oh-my-pi/pi-tui";
|
|
6
6
|
import { $env, getProjectDir, isEnoent } from "@oh-my-pi/pi-utils";
|
|
7
7
|
import { Type } from "@sinclair/typebox";
|
|
8
8
|
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
@@ -15,6 +15,7 @@ import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
|
|
|
15
15
|
import { DEFAULT_MAX_BYTES, TailBuffer } from "../session/streaming-output";
|
|
16
16
|
import { renderStatusLine } from "../tui";
|
|
17
17
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
18
|
+
import { getSixelLineMask } from "../utils/sixel";
|
|
18
19
|
import type { ToolSession } from ".";
|
|
19
20
|
import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
|
|
20
21
|
import { checkBashInterception } from "./bash-interceptor";
|
|
@@ -414,14 +415,21 @@ export const bashToolRenderer = {
|
|
|
414
415
|
|
|
415
416
|
const outputLines: string[] = [];
|
|
416
417
|
const hasOutput = displayOutput.trim().length > 0;
|
|
418
|
+
const rawOutputLines = displayOutput.split("\n");
|
|
419
|
+
const sixelLineMask =
|
|
420
|
+
TERMINAL.imageProtocol === ImageProtocol.Sixel ? getSixelLineMask(rawOutputLines) : undefined;
|
|
421
|
+
const hasSixelOutput = sixelLineMask?.some(Boolean) ?? false;
|
|
417
422
|
if (hasOutput) {
|
|
418
|
-
if (
|
|
423
|
+
if (hasSixelOutput) {
|
|
419
424
|
outputLines.push(
|
|
420
|
-
...
|
|
425
|
+
...rawOutputLines.map((line, index) =>
|
|
426
|
+
sixelLineMask?.[index] ? line : uiTheme.fg("toolOutput", replaceTabs(line)),
|
|
427
|
+
),
|
|
421
428
|
);
|
|
429
|
+
} else if (expanded) {
|
|
430
|
+
outputLines.push(...rawOutputLines.map(line => uiTheme.fg("toolOutput", replaceTabs(line))));
|
|
422
431
|
} else {
|
|
423
|
-
const styledOutput =
|
|
424
|
-
.split("\n")
|
|
432
|
+
const styledOutput = rawOutputLines
|
|
425
433
|
.map(line => uiTheme.fg("toolOutput", replaceTabs(line)))
|
|
426
434
|
.join("\n");
|
|
427
435
|
const textContent = styledOutput;
|
package/src/tui/output-block.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Bordered output container with optional header and sections.
|
|
3
3
|
*/
|
|
4
|
-
import { padding, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
4
|
+
import { ImageProtocol, padding, TERMINAL, visibleWidth } from "@oh-my-pi/pi-tui";
|
|
5
5
|
import type { Theme } from "../modes/theme/theme";
|
|
6
|
+
import { getSixelLineMask } from "../utils/sixel";
|
|
6
7
|
import type { State } from "./types";
|
|
7
8
|
import type { RenderCache } from "./utils";
|
|
8
9
|
import { getStateBgColor, Hasher, padToWidth, truncateToWidth } from "./utils";
|
|
@@ -80,7 +81,13 @@ export function renderOutputBlock(options: OutputBlockOptions, theme: Theme): st
|
|
|
80
81
|
);
|
|
81
82
|
}
|
|
82
83
|
const allLines = section.lines.flatMap(l => l.split("\n"));
|
|
83
|
-
|
|
84
|
+
const sixelLineMask = TERMINAL.imageProtocol === ImageProtocol.Sixel ? getSixelLineMask(allLines) : undefined;
|
|
85
|
+
for (let lineIndex = 0; lineIndex < allLines.length; lineIndex++) {
|
|
86
|
+
const line = allLines[lineIndex]!;
|
|
87
|
+
if (sixelLineMask?.[lineIndex]) {
|
|
88
|
+
lines.push(line);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
84
91
|
// Sections may receive content that was already padded to terminal width
|
|
85
92
|
// (e.g. from Text.render()). Trailing spaces would trigger truncateToWidth()
|
|
86
93
|
// to append an ellipsis even when the *semantic* content fits.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { $env } from "@oh-my-pi/pi-utils";
|
|
2
|
+
|
|
3
|
+
const SIXEL_START_REGEX = /\x1bP(?:[0-9;]*)q/u;
|
|
4
|
+
const SIXEL_END_SEQUENCE = "\x1b\\";
|
|
5
|
+
const SIXEL_END_BELL = "\x07";
|
|
6
|
+
const SIXEL_SEQUENCE_REGEX = /\x1bP(?:[0-9;]*)q[\s\S]*?(?:\x1b\\|\x07)/gu;
|
|
7
|
+
const SIXEL_PLACEHOLDER_PREFIX = "__OMP_SIXEL_SEQUENCE_";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Returns whether SIXEL passthrough is explicitly enabled.
|
|
11
|
+
*
|
|
12
|
+
* Both gates must be enabled to preserve SIXEL control sequences:
|
|
13
|
+
* - PI_FORCE_IMAGE_PROTOCOL=sixel
|
|
14
|
+
* - PI_ALLOW_SIXEL_PASSTHROUGH=1
|
|
15
|
+
*/
|
|
16
|
+
export function isSixelPassthroughEnabled(): boolean {
|
|
17
|
+
const forcedProtocol = $env.PI_FORCE_IMAGE_PROTOCOL?.trim().toLowerCase();
|
|
18
|
+
return forcedProtocol === "sixel" && $env.PI_ALLOW_SIXEL_PASSTHROUGH === "1";
|
|
19
|
+
}
|
|
20
|
+
/** Returns true when the text contains a SIXEL start sequence. */
|
|
21
|
+
export function containsSixelSequence(text: string): boolean {
|
|
22
|
+
return SIXEL_START_REGEX.test(text);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns a boolean mask indicating which lines belong to a SIXEL sequence block.
|
|
27
|
+
* Supports multi-line SIXEL payloads generated by libsixel.
|
|
28
|
+
*/
|
|
29
|
+
export function getSixelLineMask(lines: string[]): boolean[] {
|
|
30
|
+
let inSequence = false;
|
|
31
|
+
return lines.map(line => {
|
|
32
|
+
const hasStart = containsSixelSequence(line);
|
|
33
|
+
if (hasStart) {
|
|
34
|
+
inSequence = true;
|
|
35
|
+
}
|
|
36
|
+
const isSixelLine = inSequence;
|
|
37
|
+
if (inSequence && (line.includes(SIXEL_END_SEQUENCE) || line.includes(SIXEL_END_BELL))) {
|
|
38
|
+
inSequence = false;
|
|
39
|
+
}
|
|
40
|
+
return isSixelLine;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Returns true when the line contains a SIXEL start sequence. */
|
|
45
|
+
export function isSixelLine(line: string): boolean {
|
|
46
|
+
return containsSixelSequence(line);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Sanitizes text while preserving embedded SIXEL sequences when passthrough is enabled.
|
|
51
|
+
*/
|
|
52
|
+
export function sanitizeWithOptionalSixelPassthrough(text: string, sanitize: (text: string) => string): string {
|
|
53
|
+
if (!isSixelPassthroughEnabled() || !containsSixelSequence(text)) {
|
|
54
|
+
return sanitize(text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const preservedSequences: string[] = [];
|
|
58
|
+
const tokenized = text.replace(SIXEL_SEQUENCE_REGEX, match => {
|
|
59
|
+
const token = `${SIXEL_PLACEHOLDER_PREFIX}${preservedSequences.length}__`;
|
|
60
|
+
preservedSequences.push(match);
|
|
61
|
+
return token;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const sanitized = sanitize(tokenized);
|
|
65
|
+
return sanitized.replace(/__OMP_SIXEL_SEQUENCE_(\d+)__/gu, (_, indexText: string) => {
|
|
66
|
+
const index = Number.parseInt(indexText, 10);
|
|
67
|
+
return preservedSequences[index] ?? "";
|
|
68
|
+
});
|
|
69
|
+
}
|