@xynogen/pix-core 0.1.0 → 0.1.2
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 +50 -40
- package/src/commands/agent-sop/agent-sop.ts +58 -0
- 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 +11 -9
- package/src/lib/data.ts +1 -1
- package/src/nudge/capability.test.ts +19 -13
- package/src/nudge/capability.ts +17 -9
- 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 +31 -20
- package/src/tool/ask/components.ts +55 -0
- package/src/tool/ask/helpers.ts +77 -0
- package/src/tool/ask/index.ts +130 -0
- package/src/tool/ask/questionnaire.ts +693 -0
- package/src/tool/ask/rpc.ts +84 -0
- package/src/tool/ask/schema.ts +69 -0
- package/src/tool/ask/single-select-layout.test.ts +21 -5
- package/src/tool/ask/single-select-layout.ts +48 -14
- package/src/tool/ask/types.ts +17 -0
- package/src/tool/todo/todo.ts +24 -37
- package/src/tool/toolbox/toolbox.test.ts +2 -2
- package/src/tool/toolbox/toolbox.ts +9 -2
- package/src/ui/diagnostics.ts +3 -6
- package/src/ui/footer.ts +3 -4
- package/src/ui/welcome.test.ts +6 -6
- package/src/ui/welcome.ts +5 -2
- package/src/tool/ask/ask.ts +0 -1081
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { Params } from "./schema.js";
|
|
2
|
+
import { SENTINEL_FREEFORM } from "./schema.js";
|
|
3
|
+
import type { QuestionAnswer, QuestionnaireResult } from "./types.js";
|
|
4
|
+
|
|
5
|
+
// ── RPC / non-TUI fallback ─────────────────────────────────────────────
|
|
6
|
+
// Used when ctx.hasUI is false (headless / JSON / print mode).
|
|
7
|
+
|
|
8
|
+
export async function rpcFallback(
|
|
9
|
+
ui: { select: Function; input: Function },
|
|
10
|
+
params: Params,
|
|
11
|
+
): Promise<QuestionnaireResult> {
|
|
12
|
+
const answers: QuestionAnswer[] = [];
|
|
13
|
+
let cancelled = false;
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < params.questions.length; i++) {
|
|
16
|
+
const q = params.questions[i]!;
|
|
17
|
+
const header = q.header;
|
|
18
|
+
|
|
19
|
+
if (q.multiSelect) {
|
|
20
|
+
const lines = q.options.map(
|
|
21
|
+
(o, idx) => `${idx + 1}. ${o.label} — ${o.description}`,
|
|
22
|
+
);
|
|
23
|
+
const raw = await ui.input(
|
|
24
|
+
`${header}: ${q.question}\n\n${lines.join("\n")}\n\nEnter numbers separated by commas:`,
|
|
25
|
+
"e.g. 1,3",
|
|
26
|
+
);
|
|
27
|
+
if (raw == null) {
|
|
28
|
+
cancelled = true;
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
const indices = String(raw)
|
|
32
|
+
.split(",")
|
|
33
|
+
.map((s) => Number(s.trim()))
|
|
34
|
+
.filter((n) => n >= 1 && n <= q.options.length);
|
|
35
|
+
const selected = indices.map((n) => q.options[n - 1]?.label);
|
|
36
|
+
if (selected.length > 0) {
|
|
37
|
+
answers.push({
|
|
38
|
+
questionIndex: i,
|
|
39
|
+
question: q.question,
|
|
40
|
+
kind: "multi",
|
|
41
|
+
answer: null,
|
|
42
|
+
selected,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
cancelled = true;
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
const items = q.options.map((o) => `${o.label} — ${o.description}`);
|
|
50
|
+
items.push(SENTINEL_FREEFORM);
|
|
51
|
+
const chosen = await ui.select(`${header}: ${q.question}`, items);
|
|
52
|
+
if (chosen == null) {
|
|
53
|
+
cancelled = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (chosen === SENTINEL_FREEFORM) {
|
|
57
|
+
const text = await ui.input(q.question, "Type your answer...");
|
|
58
|
+
if (text == null) {
|
|
59
|
+
cancelled = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
answers.push({
|
|
63
|
+
questionIndex: i,
|
|
64
|
+
question: q.question,
|
|
65
|
+
kind: "custom",
|
|
66
|
+
answer: String(text),
|
|
67
|
+
});
|
|
68
|
+
} else {
|
|
69
|
+
const opt = q.options.find(
|
|
70
|
+
(o) =>
|
|
71
|
+
chosen === o.label || `${o.label} — ${o.description}` === chosen,
|
|
72
|
+
)!;
|
|
73
|
+
answers.push({
|
|
74
|
+
questionIndex: i,
|
|
75
|
+
question: q.question,
|
|
76
|
+
kind: "option",
|
|
77
|
+
answer: opt?.label ?? String(chosen),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { answers, cancelled };
|
|
84
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type Static, Type } from "typebox";
|
|
2
|
+
|
|
3
|
+
// ── Constants ──────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export const MAX_QUESTIONS = 4;
|
|
6
|
+
export const MIN_OPTIONS = 2;
|
|
7
|
+
export const MAX_OPTIONS = 4;
|
|
8
|
+
export const MAX_HEADER_LENGTH = 16;
|
|
9
|
+
export const MAX_LABEL_LENGTH = 60;
|
|
10
|
+
|
|
11
|
+
export const SENTINEL_FREEFORM = "Type something.";
|
|
12
|
+
export const SENTINEL_CHAT = "Chat about this";
|
|
13
|
+
export const SENTINEL_NEXT = "Next";
|
|
14
|
+
|
|
15
|
+
export const SPLIT_PANE_MIN_WIDTH = 84;
|
|
16
|
+
export const SEPARATOR = " │ ";
|
|
17
|
+
|
|
18
|
+
// ── Schemas ────────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export const OptionSchema = Type.Object({
|
|
21
|
+
label: Type.String({
|
|
22
|
+
maxLength: MAX_LABEL_LENGTH,
|
|
23
|
+
description: `MAX ${MAX_LABEL_LENGTH} CHARACTERS. Display text for this option. Concise (1-5 words).`,
|
|
24
|
+
}),
|
|
25
|
+
description: Type.String({
|
|
26
|
+
description: "Explanation of what this option means or trade-offs.",
|
|
27
|
+
}),
|
|
28
|
+
preview: Type.Optional(
|
|
29
|
+
Type.String({
|
|
30
|
+
description:
|
|
31
|
+
"Optional markdown preview for side-by-side layout (single-select only).",
|
|
32
|
+
}),
|
|
33
|
+
),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export const QuestionSchema = Type.Object({
|
|
37
|
+
question: Type.String({
|
|
38
|
+
description: "Clear, specific question ending with ?",
|
|
39
|
+
}),
|
|
40
|
+
header: Type.String({
|
|
41
|
+
maxLength: MAX_HEADER_LENGTH,
|
|
42
|
+
description: `MAX ${MAX_HEADER_LENGTH} CHARS — short chip/tag. E.g. "Auth method", "Approach".`,
|
|
43
|
+
}),
|
|
44
|
+
options: Type.Array(OptionSchema, {
|
|
45
|
+
minItems: MIN_OPTIONS,
|
|
46
|
+
maxItems: MAX_OPTIONS,
|
|
47
|
+
description:
|
|
48
|
+
"2-4 options. 'Type something.' and 'Chat about this' are auto-appended.",
|
|
49
|
+
}),
|
|
50
|
+
multiSelect: Type.Optional(
|
|
51
|
+
Type.Boolean({
|
|
52
|
+
default: false,
|
|
53
|
+
description:
|
|
54
|
+
"Allow multiple selections. Suppresses 'Type something.' row.",
|
|
55
|
+
}),
|
|
56
|
+
),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export const QuestionsSchema = Type.Array(QuestionSchema, {
|
|
60
|
+
minItems: 1,
|
|
61
|
+
maxItems: MAX_QUESTIONS,
|
|
62
|
+
description: "1-4 questions",
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export const ParamsSchema = Type.Object({ questions: QuestionsSchema });
|
|
66
|
+
|
|
67
|
+
export type OptionData = Static<typeof OptionSchema>;
|
|
68
|
+
export type QuestionData = Static<typeof QuestionSchema>;
|
|
69
|
+
export type Params = Static<typeof ParamsSchema>;
|
|
@@ -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
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ── Answer & result types ──────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export type AnswerKind = "option" | "custom" | "chat" | "multi";
|
|
4
|
+
|
|
5
|
+
export interface QuestionAnswer {
|
|
6
|
+
questionIndex: number;
|
|
7
|
+
question: string;
|
|
8
|
+
kind: AnswerKind;
|
|
9
|
+
answer: string | null;
|
|
10
|
+
selected?: string[];
|
|
11
|
+
preview?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface QuestionnaireResult {
|
|
15
|
+
answers: QuestionAnswer[];
|
|
16
|
+
cancelled: boolean;
|
|
17
|
+
}
|
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 ───────────────────────────────────────────────────────────────
|
|
@@ -18,6 +18,7 @@ import { dirname, join } from "node:path";
|
|
|
18
18
|
import type {
|
|
19
19
|
ExtensionAPI,
|
|
20
20
|
ExtensionContext,
|
|
21
|
+
Theme,
|
|
21
22
|
ToolInfo,
|
|
22
23
|
} from "@earendil-works/pi-coding-agent";
|
|
23
24
|
import { DynamicBorder, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
@@ -27,10 +28,12 @@ import {
|
|
|
27
28
|
fuzzyFilter,
|
|
28
29
|
Input,
|
|
29
30
|
Key,
|
|
31
|
+
type KeybindingsManager,
|
|
30
32
|
matchesKey,
|
|
31
33
|
type SelectItem,
|
|
32
34
|
SelectList,
|
|
33
35
|
Text,
|
|
36
|
+
type TUI,
|
|
34
37
|
visibleWidth,
|
|
35
38
|
} from "@earendil-works/pi-tui";
|
|
36
39
|
|
|
@@ -339,8 +342,12 @@ export default function registerToolbox(pi: ExtensionAPI): void {
|
|
|
339
342
|
};
|
|
340
343
|
}): Promise<void> {
|
|
341
344
|
await ctx.ui.custom<null>(
|
|
342
|
-
|
|
343
|
-
|
|
345
|
+
(
|
|
346
|
+
tui: TUI,
|
|
347
|
+
theme: Theme,
|
|
348
|
+
_kb: KeybindingsManager,
|
|
349
|
+
done: (r: null) => void,
|
|
350
|
+
) => {
|
|
344
351
|
const accent = "accent";
|
|
345
352
|
const mute = (s: string) => theme.fg("muted", s);
|
|
346
353
|
const container = new Container();
|
package/src/ui/diagnostics.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Registers widget with id "pi-lens" to override external pi-lens widget.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
14
14
|
|
|
15
15
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
@@ -65,10 +65,7 @@ function recordFileTouched(filePath: string): void {
|
|
|
65
65
|
|
|
66
66
|
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
67
67
|
|
|
68
|
-
function renderWidget(
|
|
69
|
-
width: number,
|
|
70
|
-
theme: { fg: (color: string, s: string) => string },
|
|
71
|
-
): string[] {
|
|
68
|
+
function renderWidget(width: number, theme: Theme): string[] {
|
|
72
69
|
const w = Math.max(1, width || 80);
|
|
73
70
|
|
|
74
71
|
const cyan = (s: string) => theme.fg("accent", s);
|
|
@@ -117,7 +114,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
117
114
|
if (!ctx.ui.setWidget) return;
|
|
118
115
|
ctx.ui.setWidget(
|
|
119
116
|
"pi-lens",
|
|
120
|
-
(tui
|
|
117
|
+
(tui, theme: Theme) => {
|
|
121
118
|
requestRenderFn = () => tui.requestRender();
|
|
122
119
|
return {
|
|
123
120
|
render: (width: number) => renderWidget(width, theme),
|
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 = {
|
package/src/ui/welcome.ts
CHANGED
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
* ✓ Ignore up to date
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import type {
|
|
30
|
+
import type {
|
|
31
|
+
ExtensionAPI,
|
|
32
|
+
ExtensionContext,
|
|
33
|
+
} from "@earendil-works/pi-coding-agent";
|
|
31
34
|
|
|
32
35
|
// ─── Theme shim (same pattern as footer.ts) ───────────────────────────────────
|
|
33
36
|
|
|
@@ -283,7 +286,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
283
286
|
let dismissed = false;
|
|
284
287
|
let requestRender: (() => void) | null = null;
|
|
285
288
|
|
|
286
|
-
const dismiss = (ctx:
|
|
289
|
+
const dismiss = (ctx: ExtensionContext) => {
|
|
287
290
|
if (dismissed) return;
|
|
288
291
|
dismissed = true;
|
|
289
292
|
requestRender = null;
|