@selesai/code 0.1.19 → 0.2.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.
Files changed (157) hide show
  1. package/dist/agents/architect.md +4 -3
  2. package/dist/agents/builder.md +5 -4
  3. package/dist/agents/commentator.md +3 -2
  4. package/dist/agents/explorer.md +4 -4
  5. package/dist/agents/recapper.md +4 -3
  6. package/dist/config.d.ts +6 -7
  7. package/dist/config.d.ts.map +1 -1
  8. package/dist/config.js +7 -9
  9. package/dist/config.js.map +1 -1
  10. package/dist/core/agent-session.d.ts.map +1 -1
  11. package/dist/core/agent-session.js +3 -1
  12. package/dist/core/agent-session.js.map +1 -1
  13. package/dist/core/compaction/compaction.d.ts.map +1 -1
  14. package/dist/core/compaction/compaction.js +18 -24
  15. package/dist/core/compaction/compaction.js.map +1 -1
  16. package/dist/core/extensions/loader.d.ts.map +1 -1
  17. package/dist/core/extensions/loader.js +2 -0
  18. package/dist/core/extensions/loader.js.map +1 -1
  19. package/dist/core/extensions/types.d.ts +8 -1
  20. package/dist/core/extensions/types.d.ts.map +1 -1
  21. package/dist/core/extensions/types.js.map +1 -1
  22. package/dist/core/skills.d.ts.map +1 -1
  23. package/dist/core/skills.js +25 -11
  24. package/dist/core/skills.js.map +1 -1
  25. package/dist/defaults/models.json +143 -39
  26. package/dist/defaults/settings.json +2 -29
  27. package/dist/extensions/package.json +5 -4
  28. package/dist/extensions/pi-intercom/index.ts +12 -5
  29. package/dist/extensions/pi-powerline-footer/index.ts +4 -1
  30. package/dist/extensions/pi-powerline-footer/tps.ts +286 -0
  31. package/dist/extensions/pi-powerline-footer/welcome.ts +47 -46
  32. package/dist/extensions/pi-subagents/README.md +12 -5
  33. package/dist/extensions/pi-subagents/agents/context-builder.md +2 -2
  34. package/dist/extensions/pi-subagents/agents/researcher.md +4 -5
  35. package/dist/extensions/pi-subagents/package.json +8 -5
  36. package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
  37. package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
  38. package/dist/extensions/pi-subagents/src/agents/agent-management.ts +1 -1
  39. package/dist/extensions/pi-subagents/src/agents/agents.ts +3 -3
  40. package/dist/extensions/pi-subagents/src/extension/control-notices.ts +1 -1
  41. package/dist/extensions/pi-subagents/src/extension/doctor.ts +3 -3
  42. package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +1 -1
  43. package/dist/extensions/pi-subagents/src/extension/index.ts +29 -2
  44. package/dist/extensions/pi-subagents/src/profiles/profiles.ts +2 -2
  45. package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +72 -13
  46. package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +1 -1
  47. package/dist/extensions/pi-subagents/src/runs/background/notify.ts +1 -1
  48. package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +85 -41
  49. package/dist/extensions/pi-subagents/src/runs/foreground/chain-clarify.ts +1 -1
  50. package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +1 -1
  51. package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +46 -3
  52. package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +2 -2
  53. package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +1 -3
  54. package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +2 -2
  55. package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +18 -18
  56. package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +24 -24
  57. package/dist/extensions/pi-subagents/src/runs/shared/spawn-timing.ts +46 -0
  58. package/dist/extensions/pi-subagents/src/runs/shared/structured-output.ts +2 -2
  59. package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +4 -4
  60. package/dist/extensions/pi-subagents/src/shared/fork-context.ts +1 -1
  61. package/dist/extensions/pi-subagents/src/shared/types.ts +6 -6
  62. package/dist/extensions/pi-subagents/src/shared/utils.ts +3 -3
  63. package/dist/extensions/pi-subagents/src/slash/slash-bridge.ts +1 -1
  64. package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +1 -1
  65. package/dist/extensions/pi-subagents/src/tui/render-helpers.ts +1 -1
  66. package/dist/extensions/pi-subagents/src/tui/render.ts +1 -1
  67. package/dist/extensions/pi-subagents/test/unit/spawn-timing.test.ts +45 -0
  68. package/dist/extensions/pi-subagents/tsconfig.runner.json +17 -0
  69. package/dist/extensions/pi-web-agent/package.json +31 -0
  70. package/dist/extensions/pi-web-agent/src/backends/config.ts +205 -0
  71. package/dist/extensions/pi-web-agent/src/backends/doctor.ts +136 -0
  72. package/dist/extensions/pi-web-agent/src/backends/factory.ts +152 -0
  73. package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +25 -0
  74. package/dist/extensions/pi-web-agent/src/cache/ttl-cache.ts +28 -0
  75. package/dist/extensions/pi-web-agent/src/changelog-notice.ts +133 -0
  76. package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +946 -0
  77. package/dist/extensions/pi-web-agent/src/extension.ts +126 -0
  78. package/dist/extensions/pi-web-agent/src/extract/readability.ts +118 -0
  79. package/dist/extensions/pi-web-agent/src/fetch/browser-resolution.ts +199 -0
  80. package/dist/extensions/pi-web-agent/src/fetch/firecrawl-fetch.ts +100 -0
  81. package/dist/extensions/pi-web-agent/src/fetch/headless-fetch.ts +117 -0
  82. package/dist/extensions/pi-web-agent/src/fetch/http-fetch.ts +67 -0
  83. package/dist/extensions/pi-web-agent/src/orchestration/answer-synthesizer.ts +60 -0
  84. package/dist/extensions/pi-web-agent/src/orchestration/candidate-selector.ts +50 -0
  85. package/dist/extensions/pi-web-agent/src/orchestration/direct-url.ts +52 -0
  86. package/dist/extensions/pi-web-agent/src/orchestration/evidence-quality.ts +105 -0
  87. package/dist/extensions/pi-web-agent/src/orchestration/evidence-ranker.ts +45 -0
  88. package/dist/extensions/pi-web-agent/src/orchestration/index.ts +28 -0
  89. package/dist/extensions/pi-web-agent/src/orchestration/query-planner.ts +47 -0
  90. package/dist/extensions/pi-web-agent/src/orchestration/research-orchestrator.ts +376 -0
  91. package/dist/extensions/pi-web-agent/src/orchestration/research-types.ts +64 -0
  92. package/dist/extensions/pi-web-agent/src/orchestration/research-worker.ts +181 -0
  93. package/dist/extensions/pi-web-agent/src/orchestration/source-profile.ts +101 -0
  94. package/dist/extensions/pi-web-agent/src/orchestration/stop-decider.ts +81 -0
  95. package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +208 -0
  96. package/dist/extensions/pi-web-agent/src/presentation/config.ts +75 -0
  97. package/dist/extensions/pi-web-agent/src/presentation/explore-presentation.ts +61 -0
  98. package/dist/extensions/pi-web-agent/src/presentation/fetch-presentation.ts +54 -0
  99. package/dist/extensions/pi-web-agent/src/presentation/search-presentation.ts +41 -0
  100. package/dist/extensions/pi-web-agent/src/presentation/select-view.ts +20 -0
  101. package/dist/extensions/pi-web-agent/src/presentation/types.ts +63 -0
  102. package/dist/extensions/pi-web-agent/src/search/brave.ts +114 -0
  103. package/dist/extensions/pi-web-agent/src/search/duckduckgo.ts +72 -0
  104. package/dist/extensions/pi-web-agent/src/search/searxng.ts +96 -0
  105. package/dist/extensions/pi-web-agent/src/tools/web-explore.ts +71 -0
  106. package/dist/extensions/pi-web-agent/src/tools/web-fetch-headless.ts +31 -0
  107. package/dist/extensions/pi-web-agent/src/tools/web-fetch.ts +31 -0
  108. package/dist/extensions/pi-web-agent/src/tools/web-search.ts +157 -0
  109. package/dist/extensions/pi-web-agent/src/types.ts +88 -0
  110. package/dist/extensions/ponytail/index.js +189 -0
  111. package/dist/extensions/ponytail/package.json +8 -0
  112. package/dist/extensions/ponytail/ponytail-config.cjs +122 -0
  113. package/dist/extensions/ponytail/ponytail-instructions.cjs +94 -0
  114. package/dist/extensions/ponytail/test/extension.test.js +167 -0
  115. package/dist/extensions/ponytail/test/helpers.test.js +92 -0
  116. package/dist/extensions/question/constants.ts +38 -0
  117. package/dist/extensions/question/dialog-adapter.ts +74 -0
  118. package/dist/extensions/question/helpers.ts +102 -0
  119. package/dist/extensions/question/index.ts +671 -0
  120. package/dist/extensions/question/navigation.ts +14 -0
  121. package/dist/extensions/question/package.json +18 -0
  122. package/dist/extensions/question/question-list.ts +498 -0
  123. package/dist/extensions/question/row-layout.ts +162 -0
  124. package/dist/extensions/question/schemas.ts +69 -0
  125. package/dist/extensions/question/selection-mode.ts +49 -0
  126. package/dist/extensions/question/shortcuts.ts +53 -0
  127. package/dist/extensions/question/tests/helpers.test.ts +178 -0
  128. package/dist/extensions/question/tests/question-list.test.ts +230 -0
  129. package/dist/extensions/question/tests/row-layout.test.ts +136 -0
  130. package/dist/extensions/question/tests/shortcuts.test.ts +85 -0
  131. package/dist/extensions/question/tests/ui-protocol.test.ts +143 -0
  132. package/dist/extensions/question/tui-adapter.ts +33 -0
  133. package/dist/extensions/question/types.ts +102 -0
  134. package/dist/extensions/question/ui-protocol.ts +46 -0
  135. package/dist/extensions/tokenin-onboarding.ts +265 -2
  136. package/dist/extensions/web-agent-onboarding.ts +222 -0
  137. package/dist/extensions/workflow/adapter.ts +588 -0
  138. package/dist/extensions/workflow/extension.ts +24 -0
  139. package/dist/extensions/workflow/modes/prototype.ts +156 -0
  140. package/dist/extensions/workflow/modes/quick.ts +138 -0
  141. package/dist/extensions/workflow/package.json +17 -0
  142. package/dist/extensions/workflow/state-machine.ts +386 -0
  143. package/dist/index.d.ts +1 -1
  144. package/dist/index.d.ts.map +1 -1
  145. package/dist/index.js +1 -1
  146. package/dist/index.js.map +1 -1
  147. package/dist/modes/interactive/interactive-mode.d.ts +2 -0
  148. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  149. package/dist/modes/interactive/interactive-mode.js +57 -13
  150. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  151. package/dist/utils/mime.d.ts.map +1 -1
  152. package/dist/utils/mime.js +24 -1
  153. package/dist/utils/mime.js.map +1 -1
  154. package/docs/docs.json +4 -0
  155. package/docs/workflows.md +206 -0
  156. package/package.json +5 -1
  157. package/README.md +0 -198
@@ -0,0 +1,136 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { wrapPlain, buildItemBlocks, flattenBlocks, renderSingleSelectRows } from "../row-layout.ts";
5
+ import type { ItemBlock, QuestionOption } from "../types.ts";
6
+
7
+ const opts = (labels: string[]): QuestionOption[] => labels.map((label) => ({ label }));
8
+
9
+ test("wrapPlain: empty → ['']", () => {
10
+ assert.deepEqual(wrapPlain("", 20), [""]);
11
+ assert.deepEqual(wrapPlain(" ", 20), [""]);
12
+ });
13
+
14
+ test("wrapPlain: single word fits", () => {
15
+ assert.deepEqual(wrapPlain("hello", 20), ["hello"]);
16
+ });
17
+
18
+ test("wrapPlain: word longer than width → chunked", () => {
19
+ assert.deepEqual(wrapPlain("abcdefgh", 3), ["abc", "def", "gh"]);
20
+ });
21
+
22
+ test("wrapPlain: multiple words wrapping", () => {
23
+ assert.deepEqual(wrapPlain("hello world foo", 7), ["hello", "world", "foo"]);
24
+ assert.deepEqual(wrapPlain("hello world foo", 11), ["hello world", "foo"]);
25
+ });
26
+
27
+ test("wrapPlain: width=1 → char-by-char", () => {
28
+ assert.deepEqual(wrapPlain("abc", 1), ["a", "b", "c"]);
29
+ });
30
+
31
+ test("buildItemBlocks: 3 options, selected=0 → arrow on block 0", () => {
32
+ const blocks = buildItemBlocks(opts(["a", "b", "c"]), 80, false, false, false, 0);
33
+ assert.equal(blocks.length, 3);
34
+ assert.equal(blocks[0]!.itemIndex, 0);
35
+ assert.ok(blocks[0]!.lines[0]!.startsWith("→"));
36
+ assert.ok(blocks[0]!.lines[0]!.includes("1. a"));
37
+ assert.ok(blocks[1]!.lines[0]!.startsWith(" "));
38
+ });
39
+
40
+ test("buildItemBlocks: selected=1 → arrow pointer on block 1", () => {
41
+ const blocks = buildItemBlocks(opts(["a", "b", "c"]), 80, false, false, false, 1);
42
+ assert.ok(blocks[1]!.lines[0]!.startsWith("→"));
43
+ assert.ok(blocks[0]!.lines[0]!.startsWith(" "));
44
+ });
45
+
46
+ test("buildItemBlocks: with description → description lines included", () => {
47
+ const blocks = buildItemBlocks(
48
+ [{ label: "a", description: "some desc" }],
49
+ 80,
50
+ false,
51
+ false,
52
+ false,
53
+ 0,
54
+ );
55
+ assert.ok(blocks[0]!.lines.length > 1);
56
+ assert.ok(blocks[0]!.lines.some((l) => l.includes("some desc")));
57
+ });
58
+
59
+ test("buildItemBlocks: hideDescriptions=true → no description lines", () => {
60
+ const blocks = buildItemBlocks(
61
+ [{ label: "a", description: "some desc" }],
62
+ 80,
63
+ false,
64
+ false,
65
+ false,
66
+ 0,
67
+ true,
68
+ );
69
+ // Only the title line, no description
70
+ assert.equal(blocks[0]!.lines.length, 1);
71
+ });
72
+
73
+ test("buildItemBlocks: allowFreeform → freeform block appended", () => {
74
+ const blocks = buildItemBlocks(opts(["a"]), 80, true, false, false, 0);
75
+ assert.equal(blocks.length, 2);
76
+ assert.ok(blocks[1]!.lines[0]!.includes("Type custom answer"));
77
+ });
78
+
79
+ test("buildItemBlocks: allowComment → comment-toggle block appended", () => {
80
+ const blocks = buildItemBlocks(opts(["a"]), 80, false, true, false, 0);
81
+ assert.equal(blocks.length, 2);
82
+ assert.ok(blocks[1]!.lines[0]!.includes("[ ]"));
83
+ assert.ok(blocks[1]!.lines[0]!.includes("Add extra context"));
84
+ });
85
+
86
+ test("buildItemBlocks: allowComment + commentEnabled → [✓]", () => {
87
+ const blocks = buildItemBlocks(opts(["a"]), 80, false, true, true, 0);
88
+ assert.ok(blocks[1]!.lines[0]!.includes("[✓]"));
89
+ });
90
+
91
+ test("flattenBlocks: correct selected flags", () => {
92
+ const blocks: ItemBlock[] = [
93
+ { itemIndex: 0, lines: ["a", "b"] },
94
+ { itemIndex: 1, lines: ["c"] },
95
+ ];
96
+ const rows = flattenBlocks(blocks, 1);
97
+ assert.equal(rows.length, 3);
98
+ assert.equal(rows[0]!.selected, false);
99
+ assert.equal(rows[1]!.selected, false);
100
+ assert.equal(rows[2]!.selected, true);
101
+ });
102
+
103
+ test("renderSingleSelectRows: few rows, all visible", () => {
104
+ const rows = renderSingleSelectRows({
105
+ options: opts(["a", "b"]),
106
+ selectedIndex: 0,
107
+ width: 80,
108
+ allowFreeform: false,
109
+ });
110
+ assert.equal(rows.length, 2);
111
+ });
112
+
113
+ test("renderSingleSelectRows: maxRows truncation with indicator", () => {
114
+ const rows = renderSingleSelectRows({
115
+ options: opts(["a", "b", "c", "c", "e", "f", "g", "h"]),
116
+ selectedIndex: 0,
117
+ width: 80,
118
+ allowFreeform: false,
119
+ maxRows: 3,
120
+ });
121
+ assert.ok(rows.length <= 3);
122
+ // Last row should be the indicator
123
+ assert.ok(rows[rows.length - 1]!.line.includes("("));
124
+ });
125
+
126
+ test("renderSingleSelectRows: selected block larger than available → only selected + indicator", () => {
127
+ const rows = renderSingleSelectRows({
128
+ options: [{ label: "a".repeat(100) }],
129
+ selectedIndex: 0,
130
+ width: 10,
131
+ allowFreeform: false,
132
+ maxRows: 3,
133
+ });
134
+ assert.ok(rows.length <= 3);
135
+ assert.ok(rows.every((r) => r.selected || r.line.includes("(")));
136
+ });
@@ -0,0 +1,85 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { normalizeShortcutSpec, isValidShortcutSpec, resolveShortcut } from "../shortcuts.ts";
5
+ import { DISABLED_SHORTCUT } from "../constants.ts";
6
+
7
+ test("normalizeShortcutSpec: undefined → undefined", () => {
8
+ assert.equal(normalizeShortcutSpec(undefined), undefined);
9
+ });
10
+
11
+ test("normalizeShortcutSpec: null → null", () => {
12
+ assert.equal(normalizeShortcutSpec(null), null);
13
+ });
14
+
15
+ test("normalizeShortcutSpec: disable values → null", () => {
16
+ assert.equal(normalizeShortcutSpec("off"), null);
17
+ assert.equal(normalizeShortcutSpec("none"), null);
18
+ assert.equal(normalizeShortcutSpec("disabled"), null);
19
+ assert.equal(normalizeShortcutSpec(""), null);
20
+ assert.equal(normalizeShortcutSpec(" off "), null);
21
+ });
22
+
23
+ test("normalizeShortcutSpec: normalizes case and trims", () => {
24
+ assert.equal(normalizeShortcutSpec("Alt+O"), "alt+o");
25
+ assert.equal(normalizeShortcutSpec(" Ctrl+G "), "ctrl+g");
26
+ });
27
+
28
+ test("isValidShortcutSpec: empty → false", () => {
29
+ assert.equal(isValidShortcutSpec(""), false);
30
+ assert.equal(isValidShortcutSpec(" "), false);
31
+ });
32
+
33
+ test("isValidShortcutSpec: valid specs", () => {
34
+ assert.equal(isValidShortcutSpec("alt+o"), true);
35
+ assert.equal(isValidShortcutSpec("ctrl+g"), true);
36
+ assert.equal(isValidShortcutSpec("shift+f1"), true);
37
+ });
38
+
39
+ test("isValidShortcutSpec: leading/trailing + → false", () => {
40
+ assert.equal(isValidShortcutSpec("+o"), false);
41
+ assert.equal(isValidShortcutSpec("o+"), false);
42
+ });
43
+
44
+ test("isValidShortcutSpec: double + → false", () => {
45
+ assert.equal(isValidShortcutSpec("alt++o"), false);
46
+ });
47
+
48
+ test("resolveShortcut: uses param when valid", () => {
49
+ const result = resolveShortcut("alt+o", undefined, "alt+o");
50
+ assert.equal(result.disabled, false);
51
+ assert.equal(result.spec, "alt+o");
52
+ });
53
+
54
+ test("resolveShortcut: param undefined, uses env", () => {
55
+ const result = resolveShortcut(undefined, "ctrl+x", "alt+o");
56
+ assert.equal(result.disabled, false);
57
+ assert.equal(result.spec, "ctrl+x");
58
+ });
59
+
60
+ test("resolveShortcut: param null → disabled", () => {
61
+ const result = resolveShortcut(null, "ctrl+x", "alt+o");
62
+ assert.equal(result, DISABLED_SHORTCUT);
63
+ });
64
+
65
+ test("resolveShortcut: param undefined, env undefined, uses default", () => {
66
+ const result = resolveShortcut(undefined, undefined, "alt+o");
67
+ assert.equal(result.disabled, false);
68
+ assert.equal(result.spec, "alt+o");
69
+ });
70
+
71
+ test("resolveShortcut: param 'off' → disabled", () => {
72
+ const result = resolveShortcut("off", "ctrl+x", "alt+o");
73
+ assert.equal(result, DISABLED_SHORTCUT);
74
+ });
75
+
76
+ test("resolveShortcut: invalid param falls through to env", () => {
77
+ const result = resolveShortcut("invalid++", "ctrl+x", "alt+o");
78
+ assert.equal(result.disabled, false);
79
+ assert.equal(result.spec, "ctrl+x");
80
+ });
81
+
82
+ test("resolveShortcut: all invalid → disabled", () => {
83
+ const result = resolveShortcut("invalid++", "also++", "default++");
84
+ assert.equal(result, DISABLED_SHORTCUT);
85
+ });
@@ -0,0 +1,143 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { DialogFallback } from "../dialog-adapter.ts";
5
+ import type { QuestionResponse, ResolvedQuestionParams } from "../types.ts";
6
+ import type { UIProtocol } from "../ui-protocol.ts";
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Fake UIProtocol — queues return values, captures calls
10
+ // ---------------------------------------------------------------------------
11
+
12
+ function fakeProtocol(overrides: Partial<UIProtocol> = {}): UIProtocol {
13
+ const calls: { method: string; args: any[] }[] = [];
14
+ return {
15
+ hasUI: true,
16
+ theme: { fg: (_n: string, s: string) => s, bold: (s: string) => s } as any,
17
+ custom: async () => undefined,
18
+ select: async (prompt: string, options: string[], opts?: any) => {
19
+ calls.push({ method: "select", args: [prompt, options, opts] });
20
+ return overrides.select?.(prompt, options, opts);
21
+ },
22
+ input: async (prompt: string, placeholder?: string, opts?: any) => {
23
+ calls.push({ method: "input", args: [prompt, placeholder, opts] });
24
+ return overrides.input?.(prompt, placeholder, opts);
25
+ },
26
+ setStatus: () => {},
27
+ notify: () => {},
28
+ ...overrides,
29
+ } as UIProtocol;
30
+ }
31
+
32
+ function makeParams(overrides: Partial<ResolvedQuestionParams> = {}): ResolvedQuestionParams {
33
+ return {
34
+ question: "Pick one",
35
+ context: undefined,
36
+ options: [{ label: "Option A" }, { label: "Option B" }],
37
+ allowMultiple: false,
38
+ allowFreeform: false,
39
+ allowComment: false,
40
+ displayMode: "overlay",
41
+ shortcuts: {
42
+ overlayToggle: { disabled: true, spec: null, matches: () => false },
43
+ commentToggle: { disabled: true, spec: null, matches: () => false },
44
+ },
45
+ timeout: undefined,
46
+ ...overrides,
47
+ };
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Single-select dialog fallback
52
+ // ---------------------------------------------------------------------------
53
+
54
+ test("DialogFallback: single-select returns selection", async () => {
55
+ const protocol = fakeProtocol({
56
+ select: async () => "Option A",
57
+ });
58
+ const result = await DialogFallback.ask(makeParams(), protocol);
59
+ assert.deepEqual(result, { kind: "selection", selections: ["Option A"] });
60
+ });
61
+
62
+ test("DialogFallback: single-select + comment", async () => {
63
+ const protocol = fakeProtocol({
64
+ select: async () => "Option A",
65
+ input: async () => "my note",
66
+ });
67
+ const result = await DialogFallback.ask(makeParams({ allowComment: true }), protocol);
68
+ assert.deepEqual(result, {
69
+ kind: "selection",
70
+ selections: ["Option A"],
71
+ comment: "my note",
72
+ });
73
+ });
74
+
75
+ test("DialogFallback: single-select cancel (select returns undefined)", async () => {
76
+ const protocol = fakeProtocol({
77
+ select: async () => undefined,
78
+ });
79
+ const result = await DialogFallback.ask(makeParams(), protocol);
80
+ assert.equal(result, null);
81
+ });
82
+
83
+ // ---------------------------------------------------------------------------
84
+ // Multi-select dialog fallback
85
+ // ---------------------------------------------------------------------------
86
+
87
+ test("DialogFallback: multi-select returns selections from input", async () => {
88
+ const protocol = fakeProtocol({
89
+ input: async () => "Option A,Option B",
90
+ });
91
+ const result = await DialogFallback.ask(makeParams({ allowMultiple: true }), protocol);
92
+ assert.deepEqual(result, {
93
+ kind: "selection",
94
+ selections: ["Option A", "Option B"],
95
+ });
96
+ });
97
+
98
+ test("DialogFallback: multi-select + comment", async () => {
99
+ let inputCall = 0;
100
+ const protocol = fakeProtocol({
101
+ input: async () => {
102
+ inputCall++;
103
+ return inputCall === 1 ? "Option A" : "extra context";
104
+ },
105
+ });
106
+ const result = await DialogFallback.ask(makeParams({ allowMultiple: true, allowComment: true }), protocol);
107
+ assert.deepEqual(result, {
108
+ kind: "selection",
109
+ selections: ["Option A"],
110
+ comment: "extra context",
111
+ });
112
+ });
113
+
114
+ test("DialogFallback: multi-select cancel (input returns undefined)", async () => {
115
+ const protocol = fakeProtocol({
116
+ input: async () => undefined,
117
+ });
118
+ const result = await DialogFallback.ask(makeParams({ allowMultiple: true }), protocol);
119
+ assert.equal(result, null);
120
+ });
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // Freeform dialog fallback
124
+ // ---------------------------------------------------------------------------
125
+
126
+ test("DialogFallback: freeform via select 'custom' option", async () => {
127
+ let selectCall = 0;
128
+ const protocol = fakeProtocol({
129
+ select: async () => "✏️ Type custom response...",
130
+ input: async () => "my custom answer",
131
+ });
132
+ const result = await DialogFallback.ask(makeParams({ allowFreeform: true }), protocol);
133
+ assert.deepEqual(result, { kind: "freeform", text: "my custom answer" });
134
+ });
135
+
136
+ test("DialogFallback: freeform cancel (input returns undefined after selecting custom)", async () => {
137
+ const protocol = fakeProtocol({
138
+ select: async () => "✏️ Type custom response...",
139
+ input: async () => undefined,
140
+ });
141
+ const result = await DialogFallback.ask(makeParams({ allowFreeform: true }), protocol);
142
+ assert.equal(result, null);
143
+ });
@@ -0,0 +1,33 @@
1
+ // TUI protocol adapter — wraps ctx.ui into UIProtocol.
2
+
3
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
4
+ import type { OverlayHandle } from "@earendil-works/pi-tui";
5
+
6
+ import { OVERLAY_MIN_WIDTH, OVERLAY_OVERLAY_MAX_HEIGHT, OVERLAY_WIDTH } from "./constants.ts";
7
+ import type { DisplayMode, QuestionResponse } from "./types.ts";
8
+ import type { CustomUIOptions, UIProtocol } from "./ui-protocol.ts";
9
+
10
+ export function buildCustomUIOptions(
11
+ displayMode: DisplayMode,
12
+ onHandle?: (handle: OverlayHandle) => void,
13
+ ): CustomUIOptions | undefined {
14
+ if (displayMode === "inline") return undefined;
15
+ return {
16
+ overlay: true,
17
+ overlayOptions: { anchor: "center", width: OVERLAY_WIDTH, minWidth: OVERLAY_MIN_WIDTH, maxHeight: OVERLAY_OVERLAY_MAX_HEIGHT, margin: 1 },
18
+ ...(onHandle ? { onHandle } : {}),
19
+ };
20
+ }
21
+
22
+ export function createTUIProtocol(ctx: ExtensionContext): UIProtocol {
23
+ return {
24
+ hasUI: ctx.hasUI,
25
+ theme: ctx.ui.theme,
26
+ custom: (factory, options) => ctx.ui.custom(factory as any, options as any) as Promise<QuestionResponse | null | undefined>,
27
+ select: (prompt, options, opts) => ctx.ui.select(prompt, options, opts),
28
+ input: (prompt, placeholder, opts) => ctx.ui.input(prompt, placeholder, opts),
29
+ onTerminalInput: typeof ctx.ui.onTerminalInput === "function" ? ctx.ui.onTerminalInput.bind(ctx.ui) : undefined,
30
+ setStatus: (key, text) => ctx.ui.setStatus(key, text),
31
+ notify: (msg, level) => ctx.ui.notify(msg, level),
32
+ };
33
+ }
@@ -0,0 +1,102 @@
1
+ // Shared type definitions for the question extension.
2
+
3
+ import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
4
+
5
+ export type RawOption = string | { label: string; description?: string };
6
+
7
+ export interface QuestionOption {
8
+ label: string;
9
+ description?: string;
10
+ }
11
+
12
+ export type QuestionResponse =
13
+ | { kind: "selection"; selections: string[]; comment?: string }
14
+ | { kind: "freeform"; text: string };
15
+
16
+ export interface QuestionDetails {
17
+ question: string;
18
+ context?: string;
19
+ options: QuestionOption[];
20
+ response: QuestionResponse | null;
21
+ cancelled: boolean;
22
+ }
23
+
24
+ export type DisplayMode = "overlay" | "inline";
25
+
26
+ export interface QuestionParams {
27
+ question: string;
28
+ context?: string;
29
+ options?: RawOption[];
30
+ allowMultiple?: boolean;
31
+ allowFreeform?: boolean;
32
+ allowComment?: boolean;
33
+ displayMode?: DisplayMode;
34
+ overlayToggleKey?: string | null;
35
+ commentToggleKey?: string | null;
36
+ timeout?: number;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Shortcut resolution
41
+ // ---------------------------------------------------------------------------
42
+
43
+ export type ResolvedShortcut =
44
+ | { disabled: false; spec: string; matches: (data: string) => boolean }
45
+ | { disabled: true; spec: null; matches: (data: string) => false };
46
+
47
+ export interface ResolvedShortcuts {
48
+ overlayToggle: ResolvedShortcut;
49
+ commentToggle: ResolvedShortcut;
50
+ }
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Single-select row layout
54
+ // ---------------------------------------------------------------------------
55
+
56
+ export interface AnnotatedRow {
57
+ line: string;
58
+ selected: boolean;
59
+ }
60
+
61
+ export interface ItemBlock {
62
+ itemIndex: number;
63
+ lines: string[];
64
+ }
65
+
66
+ export type ListItem =
67
+ | { type: "option"; option: QuestionOption }
68
+ | { type: "comment-toggle"; option: QuestionOption }
69
+ | { type: "freeform"; option: QuestionOption };
70
+
71
+ export interface RenderRowsParams {
72
+ options: QuestionOption[];
73
+ selectedIndex: number;
74
+ width: number;
75
+ allowFreeform: boolean;
76
+ allowComment?: boolean;
77
+ commentEnabled?: boolean;
78
+ maxRows?: number;
79
+ hideDescriptions?: boolean;
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Question component
84
+ // ---------------------------------------------------------------------------
85
+
86
+ export type QuestionMode = "select" | "freeform" | "comment";
87
+
88
+ // ---------------------------------------------------------------------------
89
+ // UI protocol (port for TUI vs dialog/RPC)
90
+ // ---------------------------------------------------------------------------
91
+
92
+ export interface ResolvedQuestionParams {
93
+ question: string;
94
+ context: string | undefined;
95
+ options: QuestionOption[];
96
+ allowMultiple: boolean;
97
+ allowFreeform: boolean;
98
+ allowComment: boolean;
99
+ displayMode: DisplayMode;
100
+ shortcuts: ResolvedShortcuts;
101
+ timeout: number | undefined;
102
+ }
@@ -0,0 +1,46 @@
1
+ // UIProtocol port — abstracts TUI vs dialog/RPC transport.
2
+
3
+ import type { KeybindingsManager } from "@earendil-works/pi-coding-agent";
4
+ import type { OverlayHandle, Theme, TUI } from "@earendil-works/pi-tui";
5
+
6
+ import type { QuestionResponse, ResolvedQuestionParams } from "./types.ts";
7
+
8
+ export interface CustomFactoryResult {
9
+ handleInput(data: string): void;
10
+ render(width: number): string[];
11
+ focused: boolean;
12
+ }
13
+
14
+ export type CustomFactory = (
15
+ tui: TUI,
16
+ theme: Theme,
17
+ keybindings: KeybindingsManager,
18
+ done: (result: QuestionResponse | null) => void,
19
+ ) => CustomFactoryResult;
20
+
21
+ export interface CustomUIOptions {
22
+ overlay?: boolean;
23
+ overlayOptions?: {
24
+ anchor: string;
25
+ width: string;
26
+ minWidth: number;
27
+ maxHeight: string;
28
+ margin: number;
29
+ };
30
+ onHandle?: (handle: OverlayHandle) => void;
31
+ }
32
+
33
+ export interface UIProtocol {
34
+ readonly hasUI: boolean;
35
+ readonly theme: Theme;
36
+ custom(factory: CustomFactory, options?: CustomUIOptions): Promise<QuestionResponse | null | undefined>;
37
+ select(prompt: string, options: string[], opts?: { timeout?: number }): Promise<string | undefined>;
38
+ input(prompt: string, placeholder?: string, opts?: { timeout?: number }): Promise<string | undefined>;
39
+ onTerminalInput?(handler: (data: string) => { consume: boolean } | void): () => void;
40
+ setStatus(key: string, text: string): void;
41
+ notify(message: string, level: "info" | "warning" | "error"): void;
42
+ }
43
+
44
+ export interface FallbackProtocol {
45
+ ask(params: ResolvedQuestionParams, protocol: UIProtocol): Promise<QuestionResponse | null>;
46
+ }