@xynogen/pix-core 0.1.1 → 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.
@@ -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>;
@@ -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
+ }
@@ -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,7 +342,12 @@ export default function registerToolbox(pi: ExtensionAPI): void {
339
342
  };
340
343
  }): Promise<void> {
341
344
  await ctx.ui.custom<null>(
342
- (tui: any, theme: any, _kb: unknown, done: (r: null) => void) => {
345
+ (
346
+ tui: TUI,
347
+ theme: Theme,
348
+ _kb: KeybindingsManager,
349
+ done: (r: null) => void,
350
+ ) => {
343
351
  const accent = "accent";
344
352
  const mute = (s: string) => theme.fg("muted", s);
345
353
  const container = new Container();
@@ -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: { requestRender(): void }, theme: any) => {
117
+ (tui, theme: Theme) => {
121
118
  requestRenderFn = () => tui.requestRender();
122
119
  return {
123
120
  render: (width: number) => renderWidget(width, theme),
package/src/ui/welcome.ts CHANGED
@@ -27,7 +27,10 @@
27
27
  * ✓ Ignore up to date
28
28
  */
29
29
 
30
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
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: any) => {
289
+ const dismiss = (ctx: ExtensionContext) => {
287
290
  if (dismissed) return;
288
291
  dismissed = true;
289
292
  requestRender = null;