@xynogen/pix-core 0.1.0 → 0.1.1
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 +49 -40
- package/src/commands/clear/clear.ts +1 -1
- package/src/commands/copy-all/copy-all.ts +21 -6
- package/src/commands/diff/diff.ts +45 -13
- package/src/commands/models/models.test.ts +2 -2
- package/src/commands/models/models.ts +1 -1
- package/src/commands/update/update.test.ts +8 -8
- package/src/commands/update/update.ts +4 -4
- package/src/index.ts +9 -9
- package/src/lib/data.ts +1 -1
- package/src/nudge/capability.test.ts +5 -8
- package/src/nudge/capability.ts +3 -1
- package/src/nudge/index.ts +1 -1
- package/src/nudge/tools.test.ts +21 -9
- package/src/nudge/tools.ts +0 -2
- package/src/tool/ask/ask.test.ts +29 -18
- package/src/tool/ask/ask.ts +1004 -979
- package/src/tool/ask/single-select-layout.test.ts +21 -5
- package/src/tool/ask/single-select-layout.ts +48 -14
- package/src/tool/todo/todo.ts +24 -37
- package/src/tool/toolbox/toolbox.test.ts +2 -2
- package/src/tool/toolbox/toolbox.ts +0 -1
- package/src/ui/footer.ts +3 -4
- package/src/ui/welcome.test.ts +6 -6
|
@@ -34,7 +34,11 @@ describe("renderSingleSelectRows", () => {
|
|
|
34
34
|
allowFreeform: false,
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
const rendered = rows
|
|
37
|
+
const rendered = rows
|
|
38
|
+
.map((r) => r.line)
|
|
39
|
+
.join(" ")
|
|
40
|
+
.replace(/\s+/g, " ")
|
|
41
|
+
.trim();
|
|
38
42
|
expect(rendered).toContain("want a plan first");
|
|
39
43
|
expect(rendered).toContain("before touching code");
|
|
40
44
|
expect(rows.length).toBeGreaterThan(2);
|
|
@@ -63,7 +67,12 @@ describe("renderSingleSelectRows", () => {
|
|
|
63
67
|
});
|
|
64
68
|
|
|
65
69
|
expect(rows.length).toBeLessThanOrEqual(6);
|
|
66
|
-
expect(
|
|
70
|
+
expect(
|
|
71
|
+
rows
|
|
72
|
+
.map((r) => r.line)
|
|
73
|
+
.join(" ")
|
|
74
|
+
.replace(/\s+/g, " "),
|
|
75
|
+
).toContain("troubleshooting");
|
|
67
76
|
});
|
|
68
77
|
|
|
69
78
|
test("does not duplicate a short word after wrapping an exact-width long word", () => {
|
|
@@ -79,15 +88,22 @@ describe("renderSingleSelectRows", () => {
|
|
|
79
88
|
allowFreeform: false,
|
|
80
89
|
});
|
|
81
90
|
|
|
82
|
-
expect(
|
|
83
|
-
|
|
91
|
+
expect(
|
|
92
|
+
rows.map((r) => r.line).filter((line) => line.trim() === "hi"),
|
|
93
|
+
).toHaveLength(1);
|
|
94
|
+
expect(
|
|
95
|
+
rows.map((r) => r.line).filter((line) => line.trim() === "aaaaaaaa"),
|
|
96
|
+
).toHaveLength(2);
|
|
84
97
|
});
|
|
85
98
|
|
|
86
99
|
test("marks selected item rows as selected in annotated output", () => {
|
|
87
100
|
const rows = renderSingleSelectRows({
|
|
88
101
|
options: [
|
|
89
102
|
{ title: "Alpha" },
|
|
90
|
-
{
|
|
103
|
+
{
|
|
104
|
+
title:
|
|
105
|
+
"Beta with a very long title that should wrap to multiple lines when rendered",
|
|
106
|
+
},
|
|
91
107
|
{ title: "Gamma" },
|
|
92
108
|
],
|
|
93
109
|
selectedIndex: 1,
|
|
@@ -53,7 +53,8 @@ function wrapText(text: string, width: number): string[] {
|
|
|
53
53
|
current = "";
|
|
54
54
|
for (let i = 0; i < word.length; i += width) {
|
|
55
55
|
const chunk = word.slice(i, i + width);
|
|
56
|
-
if (chunk.length === width || i + width < word.length)
|
|
56
|
+
if (chunk.length === width || i + width < word.length)
|
|
57
|
+
lines.push(chunk);
|
|
57
58
|
else current = chunk;
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -89,9 +90,15 @@ function buildItemBlocks(
|
|
|
89
90
|
const normalizedWidth = Math.max(12, width);
|
|
90
91
|
const freeformLabel = "Type something. — Enter a custom response";
|
|
91
92
|
const commentToggleLabel = `${commentEnabled ? "[✓]" : "[ ]"} Add extra context after selection`;
|
|
92
|
-
const allItems: ListItem[] = options.map((option) => ({
|
|
93
|
+
const allItems: ListItem[] = options.map((option) => ({
|
|
94
|
+
type: "option",
|
|
95
|
+
option,
|
|
96
|
+
}));
|
|
93
97
|
if (allowComment) {
|
|
94
|
-
allItems.push({
|
|
98
|
+
allItems.push({
|
|
99
|
+
type: "comment-toggle",
|
|
100
|
+
option: { title: commentToggleLabel },
|
|
101
|
+
});
|
|
95
102
|
}
|
|
96
103
|
if (allowFreeform) {
|
|
97
104
|
allItems.push({ type: "freeform", option: { title: freeformLabel } });
|
|
@@ -103,18 +110,28 @@ function buildItemBlocks(
|
|
|
103
110
|
|
|
104
111
|
if (item.type === "comment-toggle" || item.type === "freeform") {
|
|
105
112
|
const prefix = `${pointer} `;
|
|
106
|
-
const wrapped = wrapText(
|
|
113
|
+
const wrapped = wrapText(
|
|
114
|
+
item.option.title,
|
|
115
|
+
Math.max(8, normalizedWidth - prefix.length),
|
|
116
|
+
);
|
|
107
117
|
wrapped.forEach((line, lineIndex) => {
|
|
108
|
-
lines.push(
|
|
118
|
+
lines.push(
|
|
119
|
+
padLine(lineIndex === 0 ? prefix : " ".repeat(prefix.length), line),
|
|
120
|
+
);
|
|
109
121
|
});
|
|
110
122
|
return { itemIndex, lines };
|
|
111
123
|
}
|
|
112
124
|
|
|
113
125
|
const numberPrefix = `${pointer} ${itemIndex + 1}. `;
|
|
114
126
|
const continuationPrefix = " ".repeat(numberPrefix.length);
|
|
115
|
-
const titleLines = wrapText(
|
|
127
|
+
const titleLines = wrapText(
|
|
128
|
+
item.option.title,
|
|
129
|
+
Math.max(8, normalizedWidth - numberPrefix.length),
|
|
130
|
+
);
|
|
116
131
|
titleLines.forEach((line, lineIndex) => {
|
|
117
|
-
lines.push(
|
|
132
|
+
lines.push(
|
|
133
|
+
padLine(lineIndex === 0 ? numberPrefix : continuationPrefix, line),
|
|
134
|
+
);
|
|
118
135
|
});
|
|
119
136
|
|
|
120
137
|
if (item.option.description && !hideDescriptions) {
|
|
@@ -151,11 +168,25 @@ export function renderSingleSelectRows({
|
|
|
151
168
|
maxRows,
|
|
152
169
|
hideDescriptions,
|
|
153
170
|
}: RenderSingleSelectRowsParams): AnnotatedRow[] {
|
|
154
|
-
const itemCount =
|
|
155
|
-
|
|
171
|
+
const itemCount =
|
|
172
|
+
options.length + (allowComment ? 1 : 0) + (allowFreeform ? 1 : 0);
|
|
173
|
+
const blocks = buildItemBlocks(
|
|
174
|
+
options,
|
|
175
|
+
width,
|
|
176
|
+
allowFreeform,
|
|
177
|
+
allowComment,
|
|
178
|
+
commentEnabled,
|
|
179
|
+
selectedIndex,
|
|
180
|
+
hideDescriptions,
|
|
181
|
+
);
|
|
156
182
|
const allRows = flatten(blocks, selectedIndex);
|
|
157
183
|
|
|
158
|
-
if (
|
|
184
|
+
if (
|
|
185
|
+
!Number.isFinite(maxRows) ||
|
|
186
|
+
!maxRows ||
|
|
187
|
+
maxRows <= 0 ||
|
|
188
|
+
allRows.length <= maxRows
|
|
189
|
+
) {
|
|
159
190
|
return allRows;
|
|
160
191
|
}
|
|
161
192
|
|
|
@@ -180,17 +211,20 @@ export function renderSingleSelectRows({
|
|
|
180
211
|
let usedRows = selectedBlock.lines.length;
|
|
181
212
|
|
|
182
213
|
while (true) {
|
|
183
|
-
const nextCanFit =
|
|
214
|
+
const nextCanFit =
|
|
215
|
+
end < blocks.length &&
|
|
216
|
+
usedRows + blocks[end]?.lines.length <= availableRows;
|
|
184
217
|
if (nextCanFit) {
|
|
185
|
-
usedRows += blocks[end]
|
|
218
|
+
usedRows += blocks[end]?.lines.length;
|
|
186
219
|
end += 1;
|
|
187
220
|
continue;
|
|
188
221
|
}
|
|
189
222
|
|
|
190
|
-
const prevCanFit =
|
|
223
|
+
const prevCanFit =
|
|
224
|
+
start > 0 && usedRows + blocks[start - 1]?.lines.length <= availableRows;
|
|
191
225
|
if (prevCanFit) {
|
|
192
226
|
start -= 1;
|
|
193
|
-
usedRows += blocks[start]
|
|
227
|
+
usedRows += blocks[start]?.lines.length;
|
|
194
228
|
continue;
|
|
195
229
|
}
|
|
196
230
|
|
package/src/tool/todo/todo.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* checklist is seeded by the model via the tool's `set` action.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { Type } from "typebox";
|
|
13
12
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { Type } from "typebox";
|
|
14
14
|
|
|
15
15
|
export type TodoStatus = "pending" | "in_progress" | "done" | "blocked";
|
|
16
16
|
|
|
@@ -44,7 +44,9 @@ export default function registerTodo(pi: ExtensionAPI): void {
|
|
|
44
44
|
function todoSummary(): string {
|
|
45
45
|
if (!todos.length) return "(no todos)";
|
|
46
46
|
const done = todos.filter((t) => t.status === "done").length;
|
|
47
|
-
const lines = todos.map(
|
|
47
|
+
const lines = todos.map(
|
|
48
|
+
(t) => `${TODO_GLYPH[t.status]} ${t.id}. ${t.text}`,
|
|
49
|
+
);
|
|
48
50
|
return `Todos ${done}/${todos.length} done:\n${lines.join("\n")}`;
|
|
49
51
|
}
|
|
50
52
|
|
|
@@ -101,19 +103,24 @@ export default function registerTodo(pi: ExtensionAPI): void {
|
|
|
101
103
|
),
|
|
102
104
|
}),
|
|
103
105
|
async execute(_id, params) {
|
|
106
|
+
// AgentToolResult now requires a `details` field. These todo results have
|
|
107
|
+
// no structured details, so emit `undefined` via small local helpers.
|
|
108
|
+
const ok = (text: string) => ({
|
|
109
|
+
content: [{ type: "text" as const, text }],
|
|
110
|
+
details: undefined,
|
|
111
|
+
});
|
|
112
|
+
const fail = (text: string) => ({
|
|
113
|
+
content: [{ type: "text" as const, text }],
|
|
114
|
+
details: undefined,
|
|
115
|
+
isError: true,
|
|
116
|
+
});
|
|
104
117
|
switch (params.action) {
|
|
105
118
|
case "list":
|
|
106
|
-
return
|
|
119
|
+
return ok(todoSummary());
|
|
107
120
|
|
|
108
121
|
case "set": {
|
|
109
122
|
const texts = parseItems(params.items ?? "");
|
|
110
|
-
if (!texts.length)
|
|
111
|
-
return {
|
|
112
|
-
content: [
|
|
113
|
-
{ type: "text", text: "set requires non-empty `items`." },
|
|
114
|
-
],
|
|
115
|
-
isError: true,
|
|
116
|
-
};
|
|
123
|
+
if (!texts.length) return fail("set requires non-empty `items`.");
|
|
117
124
|
nextTodoId = 1;
|
|
118
125
|
todos = texts.map((text) => ({
|
|
119
126
|
id: nextTodoId++,
|
|
@@ -121,55 +128,35 @@ export default function registerTodo(pi: ExtensionAPI): void {
|
|
|
121
128
|
status: "pending" as TodoStatus,
|
|
122
129
|
}));
|
|
123
130
|
persistTodos();
|
|
124
|
-
return
|
|
131
|
+
return ok(todoSummary());
|
|
125
132
|
}
|
|
126
133
|
|
|
127
134
|
case "add": {
|
|
128
135
|
const texts = parseItems(params.items ?? "");
|
|
129
|
-
if (!texts.length)
|
|
130
|
-
return {
|
|
131
|
-
content: [
|
|
132
|
-
{ type: "text", text: "add requires non-empty `items`." },
|
|
133
|
-
],
|
|
134
|
-
isError: true,
|
|
135
|
-
};
|
|
136
|
+
if (!texts.length) return fail("add requires non-empty `items`.");
|
|
136
137
|
for (const text of texts)
|
|
137
138
|
todos.push({ id: nextTodoId++, text, status: "pending" });
|
|
138
139
|
persistTodos();
|
|
139
|
-
return
|
|
140
|
+
return ok(todoSummary());
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
case "update": {
|
|
143
144
|
const t = todos.find((x) => x.id === params.id);
|
|
144
|
-
if (!t)
|
|
145
|
-
return {
|
|
146
|
-
content: [
|
|
147
|
-
{ type: "text", text: `No todo with id ${params.id}.` },
|
|
148
|
-
],
|
|
149
|
-
isError: true,
|
|
150
|
-
};
|
|
145
|
+
if (!t) return fail(`No todo with id ${params.id}.`);
|
|
151
146
|
if (params.status) t.status = params.status;
|
|
152
147
|
if (params.text) t.text = params.text;
|
|
153
148
|
persistTodos();
|
|
154
|
-
return
|
|
149
|
+
return ok(todoSummary());
|
|
155
150
|
}
|
|
156
151
|
|
|
157
152
|
case "clear":
|
|
158
153
|
todos = [];
|
|
159
154
|
nextTodoId = 1;
|
|
160
155
|
persistTodos();
|
|
161
|
-
return
|
|
156
|
+
return ok("Todos cleared.");
|
|
162
157
|
|
|
163
158
|
default:
|
|
164
|
-
return {
|
|
165
|
-
content: [
|
|
166
|
-
{
|
|
167
|
-
type: "text",
|
|
168
|
-
text: `Unknown action: ${String(params.action)}`,
|
|
169
|
-
},
|
|
170
|
-
],
|
|
171
|
-
isError: true,
|
|
172
|
-
};
|
|
159
|
+
return fail(`Unknown action: ${String(params.action)}`);
|
|
173
160
|
}
|
|
174
161
|
},
|
|
175
162
|
});
|
|
@@ -6,9 +6,9 @@ import registerToolbox, {
|
|
|
6
6
|
buildRows,
|
|
7
7
|
parseTargets,
|
|
8
8
|
renderList,
|
|
9
|
-
toggleTool,
|
|
10
|
-
type ToolRow,
|
|
11
9
|
type ToggleOps,
|
|
10
|
+
type ToolRow,
|
|
11
|
+
toggleTool,
|
|
12
12
|
} from "./toolbox.ts";
|
|
13
13
|
|
|
14
14
|
// ─── Fixtures ───────────────────────────────────────────────────────────────
|
|
@@ -339,7 +339,6 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
339
339
|
};
|
|
340
340
|
}): Promise<void> {
|
|
341
341
|
await ctx.ui.custom<null>(
|
|
342
|
-
// biome-ignore lint: factory signature from ExtensionUIContext.custom
|
|
343
342
|
(tui: any, theme: any, _kb: unknown, done: (r: null) => void) => {
|
|
344
343
|
const accent = "accent";
|
|
345
344
|
const mute = (s: string) => theme.fg("muted", s);
|
package/src/ui/footer.ts
CHANGED
|
@@ -12,10 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { execFile } from "node:child_process";
|
|
15
|
-
import { join } from "node:path";
|
|
16
15
|
import { promisify } from "node:util";
|
|
17
|
-
import { lookupModelsDev, lookupBenchmark } from "../lib/data";
|
|
18
|
-
import type { ModelsDevModel } from "../lib/data";
|
|
19
16
|
import type {
|
|
20
17
|
AssistantMessage,
|
|
21
18
|
AssistantMessageEvent,
|
|
@@ -25,6 +22,8 @@ import type {
|
|
|
25
22
|
ReadonlyFooterDataProvider,
|
|
26
23
|
} from "@earendil-works/pi-coding-agent";
|
|
27
24
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
25
|
+
import type { ModelsDevModel } from "../lib/data";
|
|
26
|
+
import { lookupBenchmark, lookupModelsDev } from "../lib/data";
|
|
28
27
|
|
|
29
28
|
// ─── Pure formatting helpers ─────────────────────────────────────────
|
|
30
29
|
|
|
@@ -197,7 +196,7 @@ function renderBranch(
|
|
|
197
196
|
|
|
198
197
|
/** "<modelId> [· thinking] [· ctxK · $in/$out]" */
|
|
199
198
|
function renderModel(
|
|
200
|
-
model: { id?: string; provider?: string } | undefined,
|
|
199
|
+
model: { id?: string; provider?: string; name?: string } | undefined,
|
|
201
200
|
thinking: string,
|
|
202
201
|
theme: Theme,
|
|
203
202
|
): string {
|
package/src/ui/welcome.test.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { describe,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
renderCheck,
|
|
3
|
+
type CheckResult,
|
|
4
|
+
LABEL_WIDTH,
|
|
6
5
|
LOGO_ROWS,
|
|
7
6
|
PI_IGNORE_RULES,
|
|
8
|
-
|
|
7
|
+
renderCheck,
|
|
8
|
+
shortCwd,
|
|
9
|
+
statusIcon,
|
|
9
10
|
summariseTools,
|
|
10
11
|
type Theme,
|
|
11
|
-
type CheckResult,
|
|
12
12
|
} from "./welcome.ts";
|
|
13
13
|
|
|
14
14
|
const theme: Theme = {
|