@selesai/code 0.5.24 → 0.5.25

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 (50) hide show
  1. package/package.json +2 -2
  2. package/dist/core/provider-display-names.d.ts +0 -2
  3. package/dist/core/provider-display-names.d.ts.map +0 -1
  4. package/dist/core/provider-display-names.js +0 -36
  5. package/dist/core/provider-display-names.js.map +0 -1
  6. package/dist/data/capabilities.d.ts +0 -48
  7. package/dist/data/capabilities.d.ts.map +0 -1
  8. package/dist/data/capabilities.js +0 -282
  9. package/dist/data/capabilities.js.map +0 -1
  10. package/dist/data/navigation.d.ts +0 -3
  11. package/dist/data/navigation.d.ts.map +0 -1
  12. package/dist/data/navigation.js +0 -90
  13. package/dist/data/navigation.js.map +0 -1
  14. package/dist/extensions/index.d.ts +0 -3
  15. package/dist/extensions/index.d.ts.map +0 -1
  16. package/dist/extensions/index.js +0 -9
  17. package/dist/extensions/index.js.map +0 -1
  18. package/dist/extensions/llama/client.d.ts +0 -61
  19. package/dist/extensions/llama/client.d.ts.map +0 -1
  20. package/dist/extensions/llama/client.js +0 -309
  21. package/dist/extensions/llama/client.js.map +0 -1
  22. package/dist/extensions/llama/huggingface.d.ts +0 -23
  23. package/dist/extensions/llama/huggingface.d.ts.map +0 -1
  24. package/dist/extensions/llama/huggingface.js +0 -146
  25. package/dist/extensions/llama/huggingface.js.map +0 -1
  26. package/dist/extensions/llama/index.d.ts +0 -3
  27. package/dist/extensions/llama/index.d.ts.map +0 -1
  28. package/dist/extensions/llama/index.js +0 -211
  29. package/dist/extensions/llama/index.js.map +0 -1
  30. package/dist/extensions/llama/provider.d.ts +0 -10
  31. package/dist/extensions/llama/provider.d.ts.map +0 -1
  32. package/dist/extensions/llama/provider.js +0 -106
  33. package/dist/extensions/llama/provider.js.map +0 -1
  34. package/dist/extensions/llama/ui.d.ts +0 -42
  35. package/dist/extensions/llama/ui.d.ts.map +0 -1
  36. package/dist/extensions/llama/ui.js +0 -420
  37. package/dist/extensions/llama/ui.js.map +0 -1
  38. package/dist/extensions/pi-intercom/lifecycle.test.ts +0 -60
  39. package/dist/extensions/pi-subagents/src/runs/background/wait.ts +0 -353
  40. package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +0 -170
  41. package/dist/extensions/pi-subagents/test/unit/wait.test.ts +0 -398
  42. package/dist/extensions/question/dialog-adapter.ts +0 -96
  43. package/dist/extensions/question/tests/ui-protocol.test.ts +0 -157
  44. package/dist/extensions/question/tui-adapter.ts +0 -15
  45. package/dist/extensions/question/ui-protocol.ts +0 -31
  46. package/dist/extensions/workflow/modes/quick.ts +0 -150
  47. package/dist/modes/interactive/interactive-mode.test.d.ts +0 -2
  48. package/dist/modes/interactive/interactive-mode.test.d.ts.map +0 -1
  49. package/dist/modes/interactive/interactive-mode.test.js +0 -45
  50. package/dist/modes/interactive/interactive-mode.test.js.map +0 -1
@@ -1,398 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import * as fs from "node:fs";
3
- import * as os from "node:os";
4
- import * as path from "node:path";
5
- import { describe, it } from "node:test";
6
- import { waitForSubagents, type WaitDeps } from "../../src/runs/background/wait.ts";
7
- import type { SubagentState } from "../../src/shared/types.ts";
8
-
9
- function writeStatus(asyncRoot: string, runId: string, state: string, extra: object = {}): void {
10
- const dir = path.join(asyncRoot, runId);
11
- fs.mkdirSync(dir, { recursive: true });
12
- // Use a recent timestamp so the stale-run reconciler doesn't mark a live
13
- // "running" fixture as failed for having a stale heartbeat.
14
- const nowMs = Date.now();
15
- fs.writeFileSync(
16
- path.join(dir, "status.json"),
17
- JSON.stringify({
18
- runId,
19
- mode: "single",
20
- state,
21
- startedAt: nowMs,
22
- lastUpdate: nowMs,
23
- steps: [{ agent: "worker", status: state }],
24
- ...extra,
25
- }),
26
- "utf-8",
27
- );
28
- }
29
-
30
- function makeState(sessionId: string | null): SubagentState {
31
- return {
32
- baseCwd: "",
33
- currentSessionId: sessionId,
34
- asyncJobs: new Map(),
35
- foregroundControls: new Map(),
36
- lastForegroundControlId: null,
37
- cleanupTimers: new Map(),
38
- lastUiContext: null,
39
- poller: null,
40
- completionSeen: new Map(),
41
- watcher: null,
42
- watcherRestartTimer: null,
43
- resultFileCoalescer: { schedule: () => false, clear: () => {} },
44
- } as SubagentState;
45
- }
46
-
47
- function textOf(result: { content: Array<{ type: string; text?: string }> }): string {
48
- return result.content.map((c) => c.text ?? "").join("");
49
- }
50
-
51
- function baseDeps(root: string, state: SubagentState, overrides: Partial<WaitDeps> = {}): WaitDeps {
52
- return {
53
- state,
54
- asyncDirRoot: path.join(root, "runs"),
55
- resultsDir: path.join(root, "results"),
56
- // Never probe real PIDs in tests — treat every recorded pid as alive so
57
- // reconciliation doesn't flip a "running" fixture to failed.
58
- kill: () => true,
59
- pollIntervalMs: 250,
60
- ...overrides,
61
- };
62
- }
63
-
64
- describe("wait tool", () => {
65
- it("returns immediately when there is nothing to wait for", async () => {
66
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-empty-"));
67
- try {
68
- const state = makeState("sess-1");
69
- const result = await waitForSubagents({}, undefined, baseDeps(root, state));
70
- assert.equal(result.isError, undefined);
71
- assert.match(textOf(result), /nothing to wait for/i);
72
- } finally {
73
- fs.rmSync(root, { recursive: true, force: true });
74
- }
75
- });
76
-
77
- it("with all:true, resolves once every active run reaches a terminal state", async () => {
78
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-resolve-"));
79
- try {
80
- const asyncRoot = path.join(root, "runs");
81
- const state = makeState("sess-1");
82
- writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
83
- writeStatus(asyncRoot, "run-b", "queued", { sessionId: "sess-1", pid: 999998 });
84
-
85
- // Flip one run terminal on the first poll, the other on the second — so
86
- // all:true must keep waiting past the first completion.
87
- let polls = 0;
88
- const sleep = async () => {
89
- polls += 1;
90
- if (polls === 1) writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
91
- if (polls === 2) writeStatus(asyncRoot, "run-b", "failed", { sessionId: "sess-1" });
92
- };
93
-
94
- const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
95
- assert.equal(result.isError, undefined);
96
- const text = textOf(result);
97
- assert.match(text, /done/i);
98
- assert.match(text, /1 complete/);
99
- assert.match(text, /1 failed/);
100
- assert.ok(polls >= 2, "all:true should wait for both completions");
101
- } finally {
102
- fs.rmSync(root, { recursive: true, force: true });
103
- }
104
- });
105
-
106
- it("wakes when a run needs attention, not only on completion", async () => {
107
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-attn-"));
108
- try {
109
- const asyncRoot = path.join(root, "runs");
110
- const state = makeState("sess-1");
111
- // Two runs, all:true so it would normally block until both finish.
112
- writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
113
- writeStatus(asyncRoot, "run-b", "running", { sessionId: "sess-1", pid: 999998 });
114
-
115
- // Neither completes; run-a flags needs_attention (blocked for a decision).
116
- let polls = 0;
117
- const sleep = async () => {
118
- polls += 1;
119
- if (polls === 1) {
120
- writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999, activityState: "needs_attention" });
121
- }
122
- };
123
-
124
- const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
125
- assert.equal(result.isError, undefined);
126
- const text = textOf(result);
127
- assert.match(text, /need attention/i, "should report the attention run");
128
- assert.match(text, /run-a/, "should name the attention run");
129
- assert.ok(polls <= 2, `should break on attention promptly, polled ${polls}`);
130
- } finally {
131
- fs.rmSync(root, { recursive: true, force: true });
132
- }
133
- });
134
-
135
- it("reports runs that already need attention before waiting starts", async () => {
136
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-initial-attn-"));
137
- try {
138
- const asyncRoot = path.join(root, "runs");
139
- const state = makeState("sess-1");
140
- writeStatus(asyncRoot, "run-blocked", "running", { sessionId: "sess-1", pid: 999999, activityState: "needs_attention" });
141
-
142
- let polls = 0;
143
- const result = await waitForSubagents({}, undefined, baseDeps(root, state, {
144
- sleep: async () => { polls += 1; },
145
- }));
146
-
147
- assert.equal(result.isError, undefined);
148
- const text = textOf(result);
149
- assert.doesNotMatch(text, /nothing to wait for/i);
150
- assert.match(text, /need attention/i);
151
- assert.match(text, /run-blocked/);
152
- assert.equal(polls, 0, "initial attention should return without polling");
153
- } finally {
154
- fs.rmSync(root, { recursive: true, force: true });
155
- }
156
- });
157
-
158
- it("by default returns as soon as the FIRST run finishes, leaving the rest in flight", async () => {
159
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-first-"));
160
- try {
161
- const asyncRoot = path.join(root, "runs");
162
- const state = makeState("sess-1");
163
- writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
164
- writeStatus(asyncRoot, "run-b", "running", { sessionId: "sess-1", pid: 999998 });
165
- writeStatus(asyncRoot, "run-c", "running", { sessionId: "sess-1", pid: 999997 });
166
-
167
- // Only run-a finishes; b and c stay running forever.
168
- let polls = 0;
169
- const sleep = async () => {
170
- polls += 1;
171
- if (polls === 1) writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
172
- };
173
-
174
- const result = await waitForSubagents({}, undefined, baseDeps(root, state, { sleep }));
175
- assert.equal(result.isError, undefined);
176
- const text = textOf(result);
177
- assert.match(text, /1 of 3 run\(s\) finished/);
178
- assert.match(text, /1 complete/);
179
- assert.match(text, /2 run\(s\) still in flight/);
180
- // Must not have blocked on b and c: a bounded number of polls.
181
- assert.ok(polls <= 2, `first-completion should return promptly, polled ${polls}`);
182
- } finally {
183
- fs.rmSync(root, { recursive: true, force: true });
184
- }
185
- });
186
-
187
- it("only waits for runs belonging to the current session", async () => {
188
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-session-"));
189
- try {
190
- const asyncRoot = path.join(root, "runs");
191
- const state = makeState("sess-1");
192
- // A run from another session must be ignored.
193
- writeStatus(asyncRoot, "run-other", "running", { sessionId: "sess-2", pid: 999999 });
194
- const result = await waitForSubagents({}, undefined, baseDeps(root, state));
195
- assert.equal(result.isError, undefined);
196
- assert.match(textOf(result), /nothing to wait for/i);
197
- } finally {
198
- fs.rmSync(root, { recursive: true, force: true });
199
- }
200
- });
201
-
202
- it("summarizes only runs that were active when waiting began", async () => {
203
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-summary-scope-"));
204
- try {
205
- const asyncRoot = path.join(root, "runs");
206
- const state = makeState("sess-1");
207
- writeStatus(asyncRoot, "old-complete", "complete", { sessionId: "sess-1" });
208
- writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
209
-
210
- const sleep = async () => {
211
- writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
212
- };
213
-
214
- const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
215
- assert.equal(result.isError, undefined);
216
- const text = textOf(result);
217
- assert.match(text, /1 complete/);
218
- assert.doesNotMatch(text, /2 complete/);
219
- } finally {
220
- fs.rmSync(root, { recursive: true, force: true });
221
- }
222
- });
223
-
224
- it("can target a single run by id prefix", async () => {
225
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-id-"));
226
- try {
227
- const asyncRoot = path.join(root, "runs");
228
- const state = makeState("sess-1");
229
- writeStatus(asyncRoot, "run-alpha", "running", { sessionId: "sess-1", pid: 999999 });
230
- writeStatus(asyncRoot, "run-beta", "running", { sessionId: "sess-1", pid: 999998 });
231
-
232
- let polls = 0;
233
- const sleep = async () => {
234
- polls += 1;
235
- // Only alpha finishes; beta stays running but we're not waiting on it.
236
- if (polls === 1) writeStatus(asyncRoot, "run-alpha", "complete", { sessionId: "sess-1" });
237
- };
238
-
239
- const result = await waitForSubagents({ id: "run-al" }, undefined, baseDeps(root, state, { sleep }));
240
- assert.equal(result.isError, undefined);
241
- assert.match(textOf(result), /run "run-al".*done/is);
242
- } finally {
243
- fs.rmSync(root, { recursive: true, force: true });
244
- }
245
- });
246
-
247
- it("rejects ambiguous id prefixes but lets exact ids win", async () => {
248
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-ambiguous-id-"));
249
- try {
250
- const asyncRoot = path.join(root, "runs");
251
- const state = makeState("sess-1");
252
- writeStatus(asyncRoot, "run", "running", { sessionId: "sess-1", pid: 999999 });
253
- writeStatus(asyncRoot, "run-alpha", "running", { sessionId: "sess-1", pid: 999998 });
254
-
255
- const ambiguous = await waitForSubagents({ id: "ru" }, undefined, baseDeps(root, state));
256
- assert.equal(ambiguous.isError, true);
257
- assert.match(textOf(ambiguous), /Ambiguous async run id prefix "ru"/);
258
- assert.match(textOf(ambiguous), /run-alpha/);
259
-
260
- let polls = 0;
261
- const exact = await waitForSubagents({ id: "run" }, undefined, baseDeps(root, state, {
262
- sleep: async () => {
263
- polls += 1;
264
- writeStatus(asyncRoot, "run", "complete", { sessionId: "sess-1" });
265
- },
266
- }));
267
-
268
- assert.equal(exact.isError, undefined);
269
- assert.match(textOf(exact), /run "run".*done/is);
270
- assert.equal(polls, 1);
271
- } finally {
272
- fs.rmSync(root, { recursive: true, force: true });
273
- }
274
- });
275
-
276
- it("times out while runs are still active and reports them", async () => {
277
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-timeout-"));
278
- try {
279
- const asyncRoot = path.join(root, "runs");
280
- const state = makeState("sess-1");
281
- writeStatus(asyncRoot, "run-stuck", "running", { sessionId: "sess-1", pid: 999999 });
282
-
283
- // Virtual clock that jumps past the timeout on the first sleep.
284
- let clock = 0;
285
- const now = () => clock;
286
- const sleep = async (ms: number) => {
287
- clock += ms + 10_000;
288
- };
289
-
290
- const result = await waitForSubagents({ timeoutMs: 5_000 }, undefined, baseDeps(root, state, { now, sleep }));
291
- assert.equal(result.isError, true);
292
- const text = textOf(result);
293
- assert.match(text, /timed out/i);
294
- assert.match(text, /run-stuck \(running\)/);
295
- } finally {
296
- fs.rmSync(root, { recursive: true, force: true });
297
- }
298
- });
299
-
300
- it("resolves early when the turn is aborted", async () => {
301
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-abort-"));
302
- try {
303
- const asyncRoot = path.join(root, "runs");
304
- const state = makeState("sess-1");
305
- writeStatus(asyncRoot, "run-x", "running", { sessionId: "sess-1", pid: 999999 });
306
-
307
- const controller = new AbortController();
308
- const sleep = async () => {
309
- controller.abort();
310
- };
311
-
312
- const result = await waitForSubagents({}, controller.signal, baseDeps(root, state, { sleep }));
313
- assert.equal(result.isError, true);
314
- assert.match(textOf(result), /aborted/i);
315
- assert.match(textOf(result), /run-x \(running\)/);
316
- } finally {
317
- fs.rmSync(root, { recursive: true, force: true });
318
- }
319
- });
320
-
321
- it("wakes immediately on an event bus emission instead of waiting the poll interval", async () => {
322
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-event-"));
323
- try {
324
- const asyncRoot = path.join(root, "runs");
325
- const state = makeState("sess-1");
326
- writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
327
-
328
- // Fake bus. Emitting on a wake channel should end wait's sleep early.
329
- const handlers = new Map<string, Array<(d: unknown) => void>>();
330
- const events = {
331
- on(channel: string, handler: (d: unknown) => void) {
332
- const list = handlers.get(channel) ?? [];
333
- list.push(handler);
334
- handlers.set(channel, list);
335
- return () => {
336
- const l = handlers.get(channel) ?? [];
337
- handlers.set(channel, l.filter((h) => h !== handler));
338
- };
339
- },
340
- emit(channel: string, data: unknown) {
341
- for (const h of handlers.get(channel) ?? []) h(data);
342
- },
343
- };
344
-
345
- // A real timer-based sleep with a LONG poll interval; if wait waited for
346
- // the poll it would take ~10s. The event should wake it in ~10ms.
347
- let sleepCalls = 0;
348
- const realSleep = (ms: number, signal?: AbortSignal) => new Promise<void>((resolve) => {
349
- sleepCalls += 1;
350
- const t = setTimeout(resolve, ms);
351
- signal?.addEventListener("abort", () => { clearTimeout(t); resolve(); }, { once: true });
352
- });
353
-
354
- const startedAt = Date.now();
355
- const p = waitForSubagents({ all: true }, undefined, baseDeps(root, state, {
356
- events,
357
- pollIntervalMs: 10_000,
358
- sleep: realSleep,
359
- }));
360
-
361
- // After a short delay, flip the run terminal and emit a completion event.
362
- setTimeout(() => {
363
- writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
364
- events.emit("subagent:async-complete", { id: "run-a" });
365
- }, 15);
366
-
367
- const result = await p;
368
- const elapsed = Date.now() - startedAt;
369
- assert.equal(result.isError, undefined);
370
- assert.match(textOf(result), /done/i);
371
- assert.ok(elapsed < 5_000, `should wake via event, not the 10s poll; took ${elapsed}ms`);
372
- assert.ok(sleepCalls >= 1, "poll-interval sleep still armed as fallback");
373
- } finally {
374
- fs.rmSync(root, { recursive: true, force: true });
375
- }
376
- });
377
-
378
- it("still resolves via poll when no event bus is provided (fallback)", async () => {
379
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-wait-nobus-"));
380
- try {
381
- const asyncRoot = path.join(root, "runs");
382
- const state = makeState("sess-1");
383
- writeStatus(asyncRoot, "run-a", "running", { sessionId: "sess-1", pid: 999999 });
384
- let polls = 0;
385
- const sleep = async () => {
386
- polls += 1;
387
- if (polls === 1) writeStatus(asyncRoot, "run-a", "complete", { sessionId: "sess-1" });
388
- };
389
- // No `events` in deps → pure poll path.
390
- const result = await waitForSubagents({ all: true }, undefined, baseDeps(root, state, { sleep }));
391
- assert.equal(result.isError, undefined);
392
- assert.match(textOf(result), /done/i);
393
- assert.ok(polls >= 1);
394
- } finally {
395
- fs.rmSync(root, { recursive: true, force: true });
396
- }
397
- });
398
- });
@@ -1,96 +0,0 @@
1
- // Dialog/RPC fallback adapter — used when ctx.hasUI is false or custom() returns undefined.
2
-
3
- import {
4
- buildCommentPrompt,
5
- createFreeformResponse,
6
- createSelectionResponse,
7
- formatOptionsForMessage,
8
- isCancelled,
9
- parseDialogSelections,
10
- } from "./helpers.ts";
11
- import type { BatchAnswer, BatchedQuestion, QuestionOption, QuestionResponse, ResolvedQuestionParams } from "./types.ts";
12
- import type { FallbackProtocol, UIProtocol } from "./ui-protocol.ts";
13
-
14
- const FREEFORM_DIALOG_OPTION = "✏️ Type custom response...";
15
-
16
- async function askViaDialogs(
17
- ui: Pick<UIProtocol, "select" | "input">,
18
- question: string,
19
- context: string | undefined,
20
- options: QuestionOption[],
21
- allowMultiple: boolean,
22
- allowFreeform: boolean,
23
- allowComment: boolean,
24
- timeout?: number,
25
- ): Promise<QuestionResponse | null> {
26
- const dialogOpts = timeout ? { timeout } : undefined;
27
- const prompt = context ? `${question}\n\nContext:\n${context}` : question;
28
-
29
- if (allowMultiple) {
30
- const optionList = formatOptionsForMessage(options);
31
- const raw = await ui.input(
32
- `${prompt}\n\nOptions (select one or more):\n${optionList}`,
33
- "Type your selection(s)...",
34
- dialogOpts,
35
- );
36
- if (isCancelled(raw)) return null;
37
- const selections = parseDialogSelections(raw);
38
- if (selections.length === 0) return null;
39
- if (!allowComment) return createSelectionResponse(selections);
40
- const comment = await ui.input(buildCommentPrompt(prompt, selections), "Optional comment (press Enter to skip)...", dialogOpts);
41
- return createSelectionResponse(selections, comment);
42
- }
43
-
44
- const selectOptions = options.map((o) => o.label);
45
- if (allowFreeform) selectOptions.push(FREEFORM_DIALOG_OPTION);
46
-
47
- const selected = await ui.select(prompt, selectOptions, dialogOpts);
48
- if (isCancelled(selected)) return null;
49
-
50
- if (selected === FREEFORM_DIALOG_OPTION) {
51
- const answer = await ui.input(prompt, "Type your answer...", dialogOpts);
52
- if (isCancelled(answer)) return null;
53
- return createFreeformResponse(answer);
54
- }
55
-
56
- if (!allowComment) return createSelectionResponse([selected]);
57
- const comment = await ui.input(buildCommentPrompt(prompt, [selected]), "Optional comment (press Enter to skip)...", dialogOpts);
58
- return createSelectionResponse([selected], comment);
59
- }
60
-
61
- export async function askBatchViaDialogs(questions: BatchedQuestion[], protocol: UIProtocol, timeout?: number): Promise<BatchAnswer[] | null> {
62
- const deadline = timeout && timeout > 0 ? Date.now() + timeout : undefined;
63
- const answers: BatchAnswer[] = [];
64
- for (const question of questions) {
65
- const remaining = deadline === undefined ? undefined : deadline - Date.now();
66
- if (remaining !== undefined && remaining <= 0) return null;
67
- const response = await askViaDialogs(
68
- protocol,
69
- question.question,
70
- question.context,
71
- question.options,
72
- question.allowMultiple,
73
- question.allowFreeform,
74
- question.allowComment,
75
- remaining,
76
- );
77
- if (response === null) return null;
78
- answers.push({ id: question.id, question: question.question, response });
79
- }
80
- return answers;
81
- }
82
-
83
- export const DialogFallback: FallbackProtocol = {
84
- async ask(params, protocol) {
85
- return askViaDialogs(
86
- protocol,
87
- params.question,
88
- params.context,
89
- params.options,
90
- params.allowMultiple,
91
- params.allowFreeform,
92
- params.allowComment,
93
- params.timeout,
94
- );
95
- },
96
- };
@@ -1,157 +0,0 @@
1
- import test from "node:test";
2
- import assert from "node:assert/strict";
3
-
4
- import { askBatchViaDialogs, DialogFallback } from "../dialog-adapter.ts";
5
- import type { BatchedQuestion, 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
- ...overrides,
27
- } as UIProtocol;
28
- }
29
-
30
- function makeParams(overrides: Partial<ResolvedQuestionParams> = {}): ResolvedQuestionParams {
31
- return {
32
- question: "Pick one",
33
- context: undefined,
34
- options: [{ label: "Option A" }, { label: "Option B" }],
35
- allowMultiple: false,
36
- allowFreeform: false,
37
- allowComment: false,
38
- shortcuts: { commentToggle: { disabled: true, spec: null, matches: () => false } },
39
- timeout: undefined,
40
- ...overrides,
41
- };
42
- }
43
-
44
- // ---------------------------------------------------------------------------
45
- // Single-select dialog fallback
46
- // ---------------------------------------------------------------------------
47
-
48
- test("DialogFallback: single-select returns selection", async () => {
49
- const protocol = fakeProtocol({
50
- select: async () => "Option A",
51
- });
52
- const result = await DialogFallback.ask(makeParams(), protocol);
53
- assert.deepEqual(result, { kind: "selection", selections: ["Option A"] });
54
- });
55
-
56
- test("DialogFallback: single-select + comment", async () => {
57
- const protocol = fakeProtocol({
58
- select: async () => "Option A",
59
- input: async () => "my note",
60
- });
61
- const result = await DialogFallback.ask(makeParams({ allowComment: true }), protocol);
62
- assert.deepEqual(result, {
63
- kind: "selection",
64
- selections: ["Option A"],
65
- comment: "my note",
66
- });
67
- });
68
-
69
- test("DialogFallback: single-select cancel (select returns undefined)", async () => {
70
- const protocol = fakeProtocol({
71
- select: async () => undefined,
72
- });
73
- const result = await DialogFallback.ask(makeParams(), protocol);
74
- assert.equal(result, null);
75
- });
76
-
77
- // ---------------------------------------------------------------------------
78
- // Multi-select dialog fallback
79
- // ---------------------------------------------------------------------------
80
-
81
- test("DialogFallback: multi-select returns selections from input", async () => {
82
- const protocol = fakeProtocol({
83
- input: async () => "Option A,Option B",
84
- });
85
- const result = await DialogFallback.ask(makeParams({ allowMultiple: true }), protocol);
86
- assert.deepEqual(result, {
87
- kind: "selection",
88
- selections: ["Option A", "Option B"],
89
- });
90
- });
91
-
92
- test("DialogFallback: multi-select + comment", async () => {
93
- let inputCall = 0;
94
- const protocol = fakeProtocol({
95
- input: async () => {
96
- inputCall++;
97
- return inputCall === 1 ? "Option A" : "extra context";
98
- },
99
- });
100
- const result = await DialogFallback.ask(makeParams({ allowMultiple: true, allowComment: true }), protocol);
101
- assert.deepEqual(result, {
102
- kind: "selection",
103
- selections: ["Option A"],
104
- comment: "extra context",
105
- });
106
- });
107
-
108
- test("DialogFallback: multi-select cancel (input returns undefined)", async () => {
109
- const protocol = fakeProtocol({
110
- input: async () => undefined,
111
- });
112
- const result = await DialogFallback.ask(makeParams({ allowMultiple: true }), protocol);
113
- assert.equal(result, null);
114
- });
115
-
116
- // ---------------------------------------------------------------------------
117
- // Freeform dialog fallback
118
- // ---------------------------------------------------------------------------
119
-
120
- test("DialogFallback: freeform via select 'custom' option", async () => {
121
- let selectCall = 0;
122
- const protocol = fakeProtocol({
123
- select: async () => "✏️ Type custom response...",
124
- input: async () => "my custom answer",
125
- });
126
- const result = await DialogFallback.ask(makeParams({ allowFreeform: true }), protocol);
127
- assert.deepEqual(result, { kind: "freeform", text: "my custom answer" });
128
- });
129
-
130
- test("DialogFallback: freeform cancel (input returns undefined after selecting custom)", async () => {
131
- const protocol = fakeProtocol({
132
- select: async () => "✏️ Type custom response...",
133
- input: async () => undefined,
134
- });
135
- const result = await DialogFallback.ask(makeParams({ allowFreeform: true }), protocol);
136
- assert.equal(result, null);
137
- });
138
- test("askBatchViaDialogs: returns identified answers and passes remaining timeout", async () => {
139
- const timeouts: Array<number | undefined> = [];
140
- const protocol = fakeProtocol({
141
- select: async (_prompt, _options, opts) => {
142
- timeouts.push(opts?.timeout);
143
- return "Option A";
144
- },
145
- });
146
- const questions: BatchedQuestion[] = [
147
- { ...makeParams(), id: "first", label: "First" },
148
- { ...makeParams({ question: "Pick again" }), id: "second", label: "Second" },
149
- ];
150
- const result = await askBatchViaDialogs(questions, protocol, 1_000);
151
- assert.deepEqual(result, [
152
- { id: "first", question: "Pick one", response: { kind: "selection", selections: ["Option A"] } },
153
- { id: "second", question: "Pick again", response: { kind: "selection", selections: ["Option A"] } },
154
- ]);
155
- assert.equal(timeouts.length, 2);
156
- assert.ok(timeouts.every((timeout) => timeout !== undefined && timeout > 0 && timeout <= 1_000));
157
- });
@@ -1,15 +0,0 @@
1
- // TUI protocol adapter — wraps ctx.ui into UIProtocol.
2
-
3
- import type { ExtensionContext } from "@selesai/code";
4
-
5
- import type { CustomFactory, UIProtocol } from "./ui-protocol.ts";
6
-
7
- export function createTUIProtocol(ctx: ExtensionContext): UIProtocol {
8
- return {
9
- hasUI: ctx.hasUI,
10
- theme: ctx.ui.theme,
11
- custom: <T>(factory: CustomFactory<T>) => ctx.ui.custom(factory as any) as Promise<T | undefined>,
12
- select: (prompt, options, opts) => ctx.ui.select(prompt, options, opts),
13
- input: (prompt, placeholder, opts) => ctx.ui.input(prompt, placeholder, opts),
14
- };
15
- }