@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.
@@ -34,7 +34,11 @@ describe("renderSingleSelectRows", () => {
34
34
  allowFreeform: false,
35
35
  });
36
36
 
37
- const rendered = rows.map((r) => r.line).join(" ").replace(/\s+/g, " ").trim();
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(rows.map((r) => r.line).join(" ").replace(/\s+/g, " ")).toContain("troubleshooting");
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(rows.map((r) => r.line).filter((line) => line.trim() === "hi")).toHaveLength(1);
83
- expect(rows.map((r) => r.line).filter((line) => line.trim() === "aaaaaaaa")).toHaveLength(2);
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
- { title: "Beta with a very long title that should wrap to multiple lines when rendered" },
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) lines.push(chunk);
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) => ({ type: "option", option }));
93
+ const allItems: ListItem[] = options.map((option) => ({
94
+ type: "option",
95
+ option,
96
+ }));
93
97
  if (allowComment) {
94
- allItems.push({ type: "comment-toggle", option: { title: commentToggleLabel } });
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(item.option.title, Math.max(8, normalizedWidth - prefix.length));
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(padLine(lineIndex === 0 ? prefix : " ".repeat(prefix.length), line));
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(item.option.title, Math.max(8, normalizedWidth - numberPrefix.length));
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(padLine(lineIndex === 0 ? numberPrefix : continuationPrefix, line));
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 = options.length + (allowComment ? 1 : 0) + (allowFreeform ? 1 : 0);
155
- const blocks = buildItemBlocks(options, width, allowFreeform, allowComment, commentEnabled, selectedIndex, hideDescriptions);
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 (!Number.isFinite(maxRows) || !maxRows || maxRows <= 0 || allRows.length <= maxRows) {
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 = end < blocks.length && usedRows + blocks[end]!.lines.length <= availableRows;
214
+ const nextCanFit =
215
+ end < blocks.length &&
216
+ usedRows + blocks[end]?.lines.length <= availableRows;
184
217
  if (nextCanFit) {
185
- usedRows += blocks[end]!.lines.length;
218
+ usedRows += blocks[end]?.lines.length;
186
219
  end += 1;
187
220
  continue;
188
221
  }
189
222
 
190
- const prevCanFit = start > 0 && usedRows + blocks[start - 1]!.lines.length <= availableRows;
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]!.lines.length;
227
+ usedRows += blocks[start]?.lines.length;
194
228
  continue;
195
229
  }
196
230
 
@@ -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((t) => `${TODO_GLYPH[t.status]} ${t.id}. ${t.text}`);
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 { content: [{ type: "text", text: todoSummary() }] };
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 { content: [{ type: "text", text: todoSummary() }] };
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 { content: [{ type: "text", text: todoSummary() }] };
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 { content: [{ type: "text", text: todoSummary() }] };
149
+ return ok(todoSummary());
155
150
  }
156
151
 
157
152
  case "clear":
158
153
  todos = [];
159
154
  nextTodoId = 1;
160
155
  persistTodos();
161
- return { content: [{ type: "text", text: "Todos cleared." }] };
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 {
@@ -1,14 +1,14 @@
1
- import { describe, it, expect } from "bun:test";
1
+ import { describe, expect, it } from "bun:test";
2
2
  import {
3
- shortCwd,
4
- statusIcon,
5
- renderCheck,
3
+ type CheckResult,
4
+ LABEL_WIDTH,
6
5
  LOGO_ROWS,
7
6
  PI_IGNORE_RULES,
8
- LABEL_WIDTH,
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 = {