@xynogen/pix-pretty 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/package.json +2 -3
- package/src/ansi.ts +0 -10
- package/src/commands/fff.ts +60 -0
- package/src/diff-render.ts +92 -16
- package/src/image.ts +0 -3
- package/src/index.ts +88 -1452
- package/src/thinking.test.ts +153 -169
- package/src/thinking.ts +115 -68
- package/src/tools/bash.ts +154 -0
- package/src/tools/context.ts +19 -0
- package/src/tools/edit.ts +291 -0
- package/src/tools/find.ts +158 -0
- package/src/tools/grep.ts +202 -0
- package/src/tools/ls.ts +111 -0
- package/src/tools/multi-grep.ts +328 -0
- package/src/tools/read.ts +177 -0
- package/src/tools/write.ts +231 -0
- package/src/tsconfig.json +1 -1
- package/src/types.ts +30 -1
- package/src/utils.ts +45 -2
package/src/thinking.test.ts
CHANGED
|
@@ -1,190 +1,181 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for
|
|
2
|
+
* Tests for leaked-reasoning splitting into native content blocks.
|
|
3
|
+
*
|
|
4
|
+
* splitThinking() turns leaked `<think>`/`<thinking>` spans into real
|
|
5
|
+
* `thinking` content blocks (rendered dim + italic by pi's native
|
|
6
|
+
* `thinkingText` styling) while keeping surrounding answer text as `text`
|
|
7
|
+
* blocks.
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
import { describe, expect, it } from "bun:test";
|
|
6
|
-
import {
|
|
11
|
+
import { splitThinking, stripPartialTailTag } from "./thinking";
|
|
7
12
|
|
|
8
|
-
|
|
13
|
+
type Block = { type: string; text?: string; thinking?: string };
|
|
14
|
+
|
|
15
|
+
function texts(blocks: Block[]): string[] {
|
|
16
|
+
return blocks.filter((b) => b.type === "text").map((b) => b.text ?? "");
|
|
17
|
+
}
|
|
18
|
+
function thinkings(blocks: Block[]): string[] {
|
|
19
|
+
return blocks
|
|
20
|
+
.filter((b) => b.type === "thinking")
|
|
21
|
+
.map((b) => b.thinking ?? "");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("splitThinking", () => {
|
|
9
25
|
describe("closed thinking blocks", () => {
|
|
10
|
-
it("
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
it("turns a <thinking> span into a thinking block", () => {
|
|
27
|
+
const out = splitThinking("<thinking>This is reasoning</thinking>");
|
|
28
|
+
expect(out).toEqual([
|
|
29
|
+
{ type: "thinking", thinking: "This is reasoning" },
|
|
30
|
+
]);
|
|
14
31
|
});
|
|
15
32
|
|
|
16
|
-
it("
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
33
|
+
it("turns a <think> span into a thinking block", () => {
|
|
34
|
+
const out = splitThinking("<think>This is reasoning</think>");
|
|
35
|
+
expect(out).toEqual([
|
|
36
|
+
{ type: "thinking", thinking: "This is reasoning" },
|
|
37
|
+
]);
|
|
20
38
|
});
|
|
21
39
|
|
|
22
|
-
it("
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
it("preserves multi-line reasoning inside one thinking block", () => {
|
|
41
|
+
const out = splitThinking("<thinking>Line 1\nLine 2\nLine 3</thinking>");
|
|
42
|
+
expect(out).toEqual([
|
|
43
|
+
{ type: "thinking", thinking: "Line 1\nLine 2\nLine 3" },
|
|
44
|
+
]);
|
|
26
45
|
});
|
|
27
46
|
|
|
28
|
-
it("
|
|
29
|
-
const
|
|
30
|
-
"<thinking>First block</thinking> Some text <thinking>Second block</thinking>"
|
|
31
|
-
|
|
32
|
-
expect(
|
|
33
|
-
expect(
|
|
34
|
-
expect(output).toContain("Some text");
|
|
35
|
-
expect(output).toContain(">");
|
|
47
|
+
it("emits one thinking block per closed span in order", () => {
|
|
48
|
+
const out = splitThinking(
|
|
49
|
+
"<thinking>First block</thinking> Some text <thinking>Second block</thinking>",
|
|
50
|
+
);
|
|
51
|
+
expect(thinkings(out)).toEqual(["First block", "Second block"]);
|
|
52
|
+
expect(texts(out)).toEqual(["Some text"]);
|
|
36
53
|
});
|
|
37
54
|
|
|
38
|
-
it("
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
expect(
|
|
42
|
-
expect(
|
|
43
|
-
expect(output).toContain("After");
|
|
55
|
+
it("drops empty thinking spans", () => {
|
|
56
|
+
const out = splitThinking("Before <thinking></thinking> After");
|
|
57
|
+
expect(thinkings(out)).toEqual([]);
|
|
58
|
+
expect(texts(out).join(" ")).toContain("Before");
|
|
59
|
+
expect(texts(out).join(" ")).toContain("After");
|
|
44
60
|
});
|
|
45
61
|
|
|
46
|
-
it("
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
expect(output).not.toContain(">");
|
|
50
|
-
expect(output).toContain("Before");
|
|
51
|
-
expect(output).toContain("After");
|
|
62
|
+
it("drops whitespace-only thinking spans", () => {
|
|
63
|
+
const out = splitThinking("Before <thinking> \n </thinking> After");
|
|
64
|
+
expect(thinkings(out)).toEqual([]);
|
|
52
65
|
});
|
|
53
66
|
|
|
54
67
|
it("trims whitespace from thinking content", () => {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
68
|
+
const out = splitThinking(
|
|
69
|
+
"<thinking>\n This is reasoning \n</thinking>",
|
|
70
|
+
);
|
|
71
|
+
expect(out).toEqual([
|
|
72
|
+
{ type: "thinking", thinking: "This is reasoning" },
|
|
73
|
+
]);
|
|
58
74
|
});
|
|
59
75
|
|
|
60
76
|
it("handles mixed case tag names", () => {
|
|
61
|
-
const
|
|
62
|
-
"<THINKING>uppercase</THINKING> <ThInKiNg>mixedcase</ThInKiNg>"
|
|
63
|
-
|
|
64
|
-
expect(
|
|
65
|
-
expect(output).toContain("mixedcase");
|
|
66
|
-
expect(output).toContain(">");
|
|
77
|
+
const out = splitThinking(
|
|
78
|
+
"<THINKING>uppercase</THINKING> <ThInKiNg>mixedcase</ThInKiNg>",
|
|
79
|
+
);
|
|
80
|
+
expect(thinkings(out)).toEqual(["uppercase", "mixedcase"]);
|
|
67
81
|
});
|
|
68
82
|
});
|
|
69
83
|
|
|
70
84
|
describe("dangling/unclosed blocks", () => {
|
|
71
|
-
it("
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
expect(
|
|
75
|
-
expect(output).toContain("Some text");
|
|
76
|
-
expect(output).toContain(">");
|
|
85
|
+
it("treats a trailing <thinking> as a thinking block", () => {
|
|
86
|
+
const out = splitThinking("Some text <thinking>Reasoning without close");
|
|
87
|
+
expect(texts(out)).toEqual(["Some text"]);
|
|
88
|
+
expect(thinkings(out)).toEqual(["Reasoning without close"]);
|
|
77
89
|
});
|
|
78
90
|
|
|
79
|
-
it("
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
expect(
|
|
83
|
-
expect(output).toContain("Some text");
|
|
84
|
-
expect(output).toContain(">");
|
|
91
|
+
it("treats a trailing <think> as a thinking block", () => {
|
|
92
|
+
const out = splitThinking("Some text <think>Reasoning without close");
|
|
93
|
+
expect(texts(out)).toEqual(["Some text"]);
|
|
94
|
+
expect(thinkings(out)).toEqual(["Reasoning without close"]);
|
|
85
95
|
});
|
|
86
96
|
|
|
87
|
-
it("
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
expect(output).toContain(">");
|
|
97
|
+
it("captures the remainder of a leading unclosed tag as reasoning", () => {
|
|
98
|
+
const out = splitThinking("<thinking>Unclosed\nMore text after");
|
|
99
|
+
expect(thinkings(out)).toEqual(["Unclosed\nMore text after"]);
|
|
100
|
+
expect(texts(out)).toEqual([]);
|
|
92
101
|
});
|
|
93
102
|
});
|
|
94
103
|
|
|
95
104
|
describe("orphan tags", () => {
|
|
96
|
-
it("removes orphan closing tags", () => {
|
|
97
|
-
const
|
|
98
|
-
const
|
|
99
|
-
expect(
|
|
100
|
-
expect(
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
105
|
+
it("removes orphan closing tags from text", () => {
|
|
106
|
+
const out = splitThinking("Some text </thinking> more text");
|
|
107
|
+
const joined = texts(out).join(" ");
|
|
108
|
+
expect(joined).not.toContain("</thinking>");
|
|
109
|
+
expect(joined).not.toContain("<thinking>");
|
|
110
|
+
expect(joined).toContain("Some text");
|
|
111
|
+
expect(joined).toContain("more text");
|
|
103
112
|
});
|
|
104
113
|
|
|
105
|
-
it("
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
expect(
|
|
109
|
-
expect(output).toContain("Text");
|
|
114
|
+
it("treats a trailing open tag as a (possibly empty) reasoning span", () => {
|
|
115
|
+
const out = splitThinking("Text <think> orphan tag");
|
|
116
|
+
expect(texts(out)).toEqual(["Text"]);
|
|
117
|
+
expect(thinkings(out)).toEqual(["orphan tag"]);
|
|
110
118
|
});
|
|
111
119
|
|
|
112
|
-
it("
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
expect(
|
|
118
|
-
expect(
|
|
119
|
-
expect(
|
|
120
|
-
expect(
|
|
120
|
+
it("handles multiple orphan tags", () => {
|
|
121
|
+
const out = splitThinking(
|
|
122
|
+
"</thinking> text </think> more <thinking> stuff </think>",
|
|
123
|
+
);
|
|
124
|
+
const joined = texts(out).join(" ");
|
|
125
|
+
expect(joined).not.toContain("<thinking>");
|
|
126
|
+
expect(joined).not.toContain("</thinking>");
|
|
127
|
+
expect(joined).not.toContain("</think>");
|
|
128
|
+
expect(joined).toContain("text");
|
|
129
|
+
expect(thinkings(out).join(" ")).toContain("stuff");
|
|
121
130
|
});
|
|
122
131
|
});
|
|
123
132
|
|
|
124
133
|
describe("text without thinking tags", () => {
|
|
125
|
-
it("returns
|
|
134
|
+
it("returns the original text block unchanged", () => {
|
|
126
135
|
const input = "This is regular text without any tags";
|
|
127
|
-
|
|
128
|
-
expect(output).toBe(input);
|
|
136
|
+
expect(splitThinking(input)).toEqual([{ type: "text", text: input }]);
|
|
129
137
|
});
|
|
130
138
|
|
|
131
|
-
it("preserves markdown formatting", () => {
|
|
139
|
+
it("preserves markdown formatting verbatim", () => {
|
|
132
140
|
const input = "# Header\n\n**bold** and *italic*";
|
|
133
|
-
|
|
134
|
-
expect(output).toBe(input);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe("mixed content", () => {
|
|
139
|
-
it("preserves text before thinking block", () => {
|
|
140
|
-
const input = "Response text here\n\n<thinking>reasoning</thinking>";
|
|
141
|
-
const output = renderThinking(input);
|
|
142
|
-
expect(output).toContain("Response text here");
|
|
143
|
-
expect(output).toContain("reasoning");
|
|
144
|
-
expect(output).toContain(">");
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("preserves text after thinking block", () => {
|
|
148
|
-
const input = "<thinking>reasoning</thinking>\n\nMore response text";
|
|
149
|
-
const output = renderThinking(input);
|
|
150
|
-
expect(output).toContain("reasoning");
|
|
151
|
-
expect(output).toContain("More response text");
|
|
152
|
-
expect(output).toContain(">");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("preserves text between multiple thinking blocks", () => {
|
|
156
|
-
const input =
|
|
157
|
-
"<thinking>first</thinking>\n\nMiddle text\n\n<thinking>second</thinking>";
|
|
158
|
-
const output = renderThinking(input);
|
|
159
|
-
expect(output).toContain("first");
|
|
160
|
-
expect(output).toContain("Middle text");
|
|
161
|
-
expect(output).toContain("second");
|
|
162
|
-
expect(output).toContain(">");
|
|
141
|
+
expect(splitThinking(input)).toEqual([{ type: "text", text: input }]);
|
|
163
142
|
});
|
|
164
143
|
});
|
|
165
144
|
|
|
166
|
-
describe("
|
|
167
|
-
it("
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
expect(
|
|
145
|
+
describe("mixed content order", () => {
|
|
146
|
+
it("keeps text before a thinking block", () => {
|
|
147
|
+
const out = splitThinking(
|
|
148
|
+
"Response text here\n\n<thinking>reasoning</thinking>",
|
|
149
|
+
);
|
|
150
|
+
expect(out).toEqual([
|
|
151
|
+
{ type: "text", text: "Response text here" },
|
|
152
|
+
{ type: "thinking", thinking: "reasoning" },
|
|
153
|
+
]);
|
|
172
154
|
});
|
|
173
155
|
|
|
174
|
-
it("
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
156
|
+
it("keeps text after a thinking block", () => {
|
|
157
|
+
const out = splitThinking(
|
|
158
|
+
"<thinking>reasoning</thinking>\n\nMore response text",
|
|
159
|
+
);
|
|
160
|
+
expect(out).toEqual([
|
|
161
|
+
{ type: "thinking", thinking: "reasoning" },
|
|
162
|
+
{ type: "text", text: "More response text" },
|
|
163
|
+
]);
|
|
178
164
|
});
|
|
179
165
|
|
|
180
|
-
it("
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
166
|
+
it("keeps text between multiple thinking blocks in order", () => {
|
|
167
|
+
const out = splitThinking(
|
|
168
|
+
"<thinking>first</thinking>\n\nMiddle text\n\n<thinking>second</thinking>",
|
|
169
|
+
);
|
|
170
|
+
expect(out).toEqual([
|
|
171
|
+
{ type: "thinking", thinking: "first" },
|
|
172
|
+
{ type: "text", text: "Middle text" },
|
|
173
|
+
{ type: "thinking", thinking: "second" },
|
|
174
|
+
]);
|
|
184
175
|
});
|
|
185
176
|
});
|
|
186
177
|
|
|
187
|
-
describe("streaming (partial tail tags
|
|
178
|
+
describe("streaming (partial tail tags)", () => {
|
|
188
179
|
it("strips a half-streamed opening tag", () => {
|
|
189
180
|
expect(stripPartialTailTag("Hello <thin")).toBe("Hello ");
|
|
190
181
|
expect(stripPartialTailTag("Hello <")).toBe("Hello ");
|
|
@@ -205,63 +196,56 @@ describe("thinking tag rendering", () => {
|
|
|
205
196
|
expect(stripPartialTailTag("<thinking>body")).toBe("<thinking>body");
|
|
206
197
|
});
|
|
207
198
|
|
|
208
|
-
it("
|
|
209
|
-
// Simulates mid-stream state: open tag + partial body, no close tag.
|
|
199
|
+
it("emits a thinking block for an open span before the close arrives", () => {
|
|
210
200
|
const midStream = "<thinking>I am reasoning about";
|
|
211
|
-
const
|
|
212
|
-
expect(
|
|
201
|
+
const out = splitThinking(stripPartialTailTag(midStream));
|
|
202
|
+
expect(out).toEqual([
|
|
203
|
+
{ type: "thinking", thinking: "I am reasoning about" },
|
|
204
|
+
]);
|
|
213
205
|
});
|
|
214
206
|
|
|
215
|
-
it("renders progressively without flashing partial close tag", () => {
|
|
216
|
-
const step1 =
|
|
217
|
-
const step2 =
|
|
207
|
+
it("renders progressively without flashing a partial close tag", () => {
|
|
208
|
+
const step1 = splitThinking(stripPartialTailTag("<think>step one"));
|
|
209
|
+
const step2 = splitThinking(
|
|
218
210
|
stripPartialTailTag("<think>step one and two</thi"),
|
|
219
211
|
);
|
|
220
|
-
const step3 =
|
|
212
|
+
const step3 = splitThinking(
|
|
221
213
|
stripPartialTailTag("<think>step one and two</think>\n\nAnswer"),
|
|
222
214
|
);
|
|
223
|
-
expect(step1).
|
|
224
|
-
expect(step2).
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
expect(step3).
|
|
215
|
+
expect(step1).toEqual([{ type: "thinking", thinking: "step one" }]);
|
|
216
|
+
expect(step2).toEqual([
|
|
217
|
+
{ type: "thinking", thinking: "step one and two" },
|
|
218
|
+
]);
|
|
219
|
+
expect(step3).toEqual([
|
|
220
|
+
{ type: "thinking", thinking: "step one and two" },
|
|
221
|
+
{ type: "text", text: "Answer" },
|
|
222
|
+
]);
|
|
228
223
|
});
|
|
229
224
|
});
|
|
230
225
|
|
|
231
226
|
describe("edge cases", () => {
|
|
232
|
-
it("
|
|
233
|
-
|
|
234
|
-
const output = renderThinking(input);
|
|
235
|
-
expect(output).toBe("");
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it("handles string with only whitespace", () => {
|
|
239
|
-
const input = " \n \n ";
|
|
240
|
-
const output = renderThinking(input);
|
|
241
|
-
expect(output).toBe("");
|
|
227
|
+
it("returns a single text block for an empty string", () => {
|
|
228
|
+
expect(splitThinking("")).toEqual([{ type: "text", text: "" }]);
|
|
242
229
|
});
|
|
243
230
|
|
|
244
|
-
it("
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
|
|
248
|
-
// Regex will match first <thinking>...</thinking> pair
|
|
249
|
-
expect(output).toContain(">");
|
|
231
|
+
it("collapses an all-empty reasoning message to one empty text block", () => {
|
|
232
|
+
expect(splitThinking("<thinking></thinking>")).toEqual([
|
|
233
|
+
{ type: "text", text: "" },
|
|
234
|
+
]);
|
|
250
235
|
});
|
|
251
236
|
|
|
252
237
|
it("handles thinking content with special characters", () => {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
expect(
|
|
238
|
+
const out = splitThinking(
|
|
239
|
+
"<thinking>Special chars: $@#%^&*()</thinking>",
|
|
240
|
+
);
|
|
241
|
+
expect(thinkings(out)).toEqual(["Special chars: $@#%^&*()"]);
|
|
257
242
|
});
|
|
258
243
|
|
|
259
244
|
it("handles thinking content with code-like syntax", () => {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
expect(
|
|
264
|
-
expect(output).toContain(">");
|
|
245
|
+
const out = splitThinking(
|
|
246
|
+
"<thinking>const x = 5;\nreturn x + 1;</thinking>",
|
|
247
|
+
);
|
|
248
|
+
expect(thinkings(out)).toEqual(["const x = 5;\nreturn x + 1;"]);
|
|
265
249
|
});
|
|
266
250
|
});
|
|
267
251
|
});
|