@duetso/agent 0.1.81 → 0.1.82

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 (82) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/src/memory/observational.d.ts +7 -0
  3. package/dist/src/memory/observational.d.ts.map +1 -1
  4. package/dist/src/memory/observational.js +7 -1
  5. package/dist/src/memory/observational.js.map +1 -1
  6. package/dist/src/tui/app.d.ts +25 -35
  7. package/dist/src/tui/app.d.ts.map +1 -1
  8. package/dist/src/tui/app.js +195 -2308
  9. package/dist/src/tui/app.js.map +1 -1
  10. package/dist/src/tui/autocomplete-controller.d.ts +83 -0
  11. package/dist/src/tui/autocomplete-controller.d.ts.map +1 -0
  12. package/dist/src/tui/autocomplete-controller.js +308 -0
  13. package/dist/src/tui/autocomplete-controller.js.map +1 -0
  14. package/dist/src/tui/boot-screen.d.ts +64 -0
  15. package/dist/src/tui/boot-screen.d.ts.map +1 -0
  16. package/dist/src/tui/boot-screen.js +137 -0
  17. package/dist/src/tui/boot-screen.js.map +1 -0
  18. package/dist/src/tui/controllers.d.ts +39 -0
  19. package/dist/src/tui/controllers.d.ts.map +1 -0
  20. package/dist/src/tui/controllers.js +53 -0
  21. package/dist/src/tui/controllers.js.map +1 -0
  22. package/dist/src/tui/copy-controller.d.ts +75 -0
  23. package/dist/src/tui/copy-controller.d.ts.map +1 -0
  24. package/dist/src/tui/copy-controller.js +140 -0
  25. package/dist/src/tui/copy-controller.js.map +1 -0
  26. package/dist/src/tui/history-replay.d.ts +33 -0
  27. package/dist/src/tui/history-replay.d.ts.map +1 -0
  28. package/dist/src/tui/history-replay.js +47 -0
  29. package/dist/src/tui/history-replay.js.map +1 -0
  30. package/dist/src/tui/initial-prompt.d.ts +24 -0
  31. package/dist/src/tui/initial-prompt.d.ts.map +1 -0
  32. package/dist/src/tui/initial-prompt.js +29 -0
  33. package/dist/src/tui/initial-prompt.js.map +1 -0
  34. package/dist/src/tui/key-handlers.d.ts +49 -0
  35. package/dist/src/tui/key-handlers.d.ts.map +1 -0
  36. package/dist/src/tui/key-handlers.js +192 -0
  37. package/dist/src/tui/key-handlers.js.map +1 -0
  38. package/dist/src/tui/layout.d.ts +71 -0
  39. package/dist/src/tui/layout.d.ts.map +1 -0
  40. package/dist/src/tui/layout.js +245 -0
  41. package/dist/src/tui/layout.js.map +1 -0
  42. package/dist/src/tui/paste-controller.d.ts +90 -0
  43. package/dist/src/tui/paste-controller.d.ts.map +1 -0
  44. package/dist/src/tui/paste-controller.js +234 -0
  45. package/dist/src/tui/paste-controller.js.map +1 -0
  46. package/dist/src/tui/question-picker.d.ts +111 -0
  47. package/dist/src/tui/question-picker.d.ts.map +1 -0
  48. package/dist/src/tui/question-picker.js +308 -0
  49. package/dist/src/tui/question-picker.js.map +1 -0
  50. package/dist/src/tui/renderer-lifecycle.d.ts +20 -0
  51. package/dist/src/tui/renderer-lifecycle.d.ts.map +1 -0
  52. package/dist/src/tui/renderer-lifecycle.js +50 -0
  53. package/dist/src/tui/renderer-lifecycle.js.map +1 -0
  54. package/dist/src/tui/session-subscription.d.ts +30 -0
  55. package/dist/src/tui/session-subscription.d.ts.map +1 -0
  56. package/dist/src/tui/session-subscription.js +78 -0
  57. package/dist/src/tui/session-subscription.js.map +1 -0
  58. package/dist/src/tui/slash-commands.d.ts +23 -0
  59. package/dist/src/tui/slash-commands.d.ts.map +1 -0
  60. package/dist/src/tui/slash-commands.js +79 -0
  61. package/dist/src/tui/slash-commands.js.map +1 -0
  62. package/dist/src/tui/starter-section.d.ts +99 -0
  63. package/dist/src/tui/starter-section.d.ts.map +1 -0
  64. package/dist/src/tui/starter-section.js +254 -0
  65. package/dist/src/tui/starter-section.js.map +1 -0
  66. package/dist/src/tui/status-controller.d.ts +94 -0
  67. package/dist/src/tui/status-controller.d.ts.map +1 -0
  68. package/dist/src/tui/status-controller.js +186 -0
  69. package/dist/src/tui/status-controller.js.map +1 -0
  70. package/dist/src/tui/step-renderer.d.ts +55 -0
  71. package/dist/src/tui/step-renderer.d.ts.map +1 -0
  72. package/dist/src/tui/step-renderer.js +246 -0
  73. package/dist/src/tui/step-renderer.js.map +1 -0
  74. package/dist/src/tui/transcript-writer.d.ts +63 -0
  75. package/dist/src/tui/transcript-writer.d.ts.map +1 -0
  76. package/dist/src/tui/transcript-writer.js +130 -0
  77. package/dist/src/tui/transcript-writer.js.map +1 -0
  78. package/dist/src/turn-runner/turn-runner.d.ts +43 -1
  79. package/dist/src/turn-runner/turn-runner.d.ts.map +1 -1
  80. package/dist/src/turn-runner/turn-runner.js +86 -4
  81. package/dist/src/turn-runner/turn-runner.js.map +1 -1
  82. package/package.json +1 -1
@@ -0,0 +1,90 @@
1
+ import { type PasteEvent, type TextareaRenderable } from "@opentui/core";
2
+ import { type PendingImage } from "./paste.js";
3
+ import type { StatusController } from "./status-controller.js";
4
+ export interface PasteControllerOptions {
5
+ /** Composer textarea. The controller inserts `[Image #N]` placeholders
6
+ * into its buffer and falls back to text inserts when a clipboard probe
7
+ * yields plain text after a keystroke ate a paste event. */
8
+ inputField: TextareaRenderable;
9
+ /** Session id used to namespace the on-disk paste cache under
10
+ * `~/.duet/cache/paste/<sessionId>/`. */
11
+ sessionId: string;
12
+ /** Working directory for resolving relative paths handed to `/image`. */
13
+ workDir: string;
14
+ /** Append a bordered transcript block (label + body) for paste diagnostics
15
+ * and per-attachment confirmation lines. */
16
+ appendBlock: (label: string | null, body: string, color: string) => void;
17
+ /** Status surface that owns the bottom-hint attachment counter. The
18
+ * controller calls `setPendingImageCount` whenever the queue changes so
19
+ * the hint stays in sync. */
20
+ statusController: Pick<StatusController, "setPendingImageCount">;
21
+ }
22
+ /**
23
+ * Owns the pending-image queue surfaced into the prompt as `[Image #N]`
24
+ * placeholders. Handles three intake paths that converge on the same
25
+ * `persistPastedImage` / `loadImageFromPath` machinery:
26
+ *
27
+ * 1. Bracketed-paste events that carry binary image bytes (kitty, ghostty,
28
+ * recent iTerm2).
29
+ * 2. Text-shaped pastes that resolve to a single existing image file path
30
+ * (Finder/Files drag-paste, file:// URLs).
31
+ * 3. Manual probes triggered by Cmd+V/Ctrl+V or the `/paste` slash command
32
+ * against terminals that strip binary clipboards.
33
+ *
34
+ * The queue resets after every submit (callers drive that via `consume()`),
35
+ * so users see a fresh `#1` label per turn.
36
+ */
37
+ export declare class PasteController {
38
+ private readonly inputField;
39
+ private readonly sessionId;
40
+ private readonly workDir;
41
+ private readonly appendBlock;
42
+ private readonly statusController;
43
+ private pendingImages;
44
+ /** Monotonic id used to label the next attachment. Reset together with
45
+ * `pendingImages` so labels restart at `#1` after each submit. */
46
+ private nextImageId;
47
+ constructor(options: PasteControllerOptions);
48
+ /** Read-only view of the currently queued attachments. Useful for callers
49
+ * that need to render attachment footnotes alongside the user message. */
50
+ attachments(): readonly PendingImage[];
51
+ /**
52
+ * Drain the queue, returning the attachments that were pending at submit
53
+ * time. The caller forwards `.attachment` payloads to the runner; the
54
+ * controller resets so the next turn starts at `[Image #1]`.
55
+ */
56
+ consume(): PendingImage[];
57
+ /** Drop all queued attachments. No-op when the queue is already empty so
58
+ * `/clear-images` does not redundantly repaint the hint. */
59
+ clearPendingImages(): void;
60
+ /**
61
+ * Handle a bracketed-paste event from the textarea. Decides synchronously
62
+ * whether the payload is image bytes, a file path, an empty trigger that
63
+ * warrants a clipboard probe, or plain text that should fall through to
64
+ * the textarea's default insert path.
65
+ */
66
+ handlePasteEvent(event: PasteEvent): Promise<void>;
67
+ /**
68
+ * Manual clipboard probe. Read the OS clipboard for an image right now and
69
+ * attach it if found; otherwise fall back to a text probe so a Cmd+V/Ctrl+V
70
+ * keystroke that ate a legitimate text paste does not silently swallow it.
71
+ * The `source` distinguishes the keystroke entry point from the `/paste`
72
+ * slash command so we only emit the "no readable image" diagnostic for
73
+ * explicit user intent.
74
+ */
75
+ triggerClipboardProbe(source: "keystroke" | "slash"): Promise<void>;
76
+ /**
77
+ * Attach an image from disk by path. Drives the `/image <path>` slash
78
+ * command. Inserts the `[Image #N]` placeholder back into the (likely now
79
+ * empty) input so the user can keep typing their prompt with the image
80
+ * already attached.
81
+ */
82
+ attachImageFromPath(rawPath: string): Promise<void>;
83
+ /**
84
+ * Persist raw image bytes (from a bracketed-paste event or clipboard probe)
85
+ * under the session cache and surface the resulting `[Image #N]`
86
+ * placeholder in the prompt buffer.
87
+ */
88
+ private attachPastedImageBytes;
89
+ }
90
+ //# sourceMappingURL=paste-controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paste-controller.d.ts","sourceRoot":"","sources":["../../../src/tui/paste-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,UAAU,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAE3F,OAAO,EAIL,KAAK,YAAY,EAKlB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAG/D,MAAM,WAAW,sBAAsB;IACrC;;iEAE6D;IAC7D,UAAU,EAAE,kBAAkB,CAAC;IAC/B;8CAC0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,OAAO,EAAE,MAAM,CAAC;IAChB;iDAC6C;IAC7C,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE;;kCAE8B;IAC9B,gBAAgB,EAAE,IAAI,CAAC,gBAAgB,EAAE,sBAAsB,CAAC,CAAC;CAClE;AAED;;;;;;;;;;;;;;GAcG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAqB;IAChD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IACnC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA8D;IAC1F,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAiD;IAElF,OAAO,CAAC,aAAa,CAAsB;IAC3C;uEACmE;IACnE,OAAO,CAAC,WAAW,CAAK;gBAEZ,OAAO,EAAE,sBAAsB;IAQ3C;+EAC2E;IAC3E,WAAW,IAAI,SAAS,YAAY,EAAE;IAItC;;;;OAIG;IACH,OAAO,IAAI,YAAY,EAAE;IAQzB;iEAC6D;IAC7D,kBAAkB,IAAI,IAAI;IAO1B;;;;;OAKG;IACG,gBAAgB,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC;IA8FxD;;;;;;;OAOG;IACG,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAsCzE;;;;;OAKG;IACG,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBzD;;;;OAIG;YACW,sBAAsB;CAyBrC"}
@@ -0,0 +1,234 @@
1
+ import { decodePasteBytes } from "@opentui/core";
2
+ import { describeMacClipboardTypes, loadImageFromPath, looksLikeImageFilePath, persistPastedImage, sniffImageMimeType, tryReadClipboardImage, tryReadClipboardText, } from "./paste.js";
3
+ import { COLORS } from "./theme.js";
4
+ /**
5
+ * Owns the pending-image queue surfaced into the prompt as `[Image #N]`
6
+ * placeholders. Handles three intake paths that converge on the same
7
+ * `persistPastedImage` / `loadImageFromPath` machinery:
8
+ *
9
+ * 1. Bracketed-paste events that carry binary image bytes (kitty, ghostty,
10
+ * recent iTerm2).
11
+ * 2. Text-shaped pastes that resolve to a single existing image file path
12
+ * (Finder/Files drag-paste, file:// URLs).
13
+ * 3. Manual probes triggered by Cmd+V/Ctrl+V or the `/paste` slash command
14
+ * against terminals that strip binary clipboards.
15
+ *
16
+ * The queue resets after every submit (callers drive that via `consume()`),
17
+ * so users see a fresh `#1` label per turn.
18
+ */
19
+ export class PasteController {
20
+ inputField;
21
+ sessionId;
22
+ workDir;
23
+ appendBlock;
24
+ statusController;
25
+ pendingImages = [];
26
+ /** Monotonic id used to label the next attachment. Reset together with
27
+ * `pendingImages` so labels restart at `#1` after each submit. */
28
+ nextImageId = 1;
29
+ constructor(options) {
30
+ this.inputField = options.inputField;
31
+ this.sessionId = options.sessionId;
32
+ this.workDir = options.workDir;
33
+ this.appendBlock = options.appendBlock;
34
+ this.statusController = options.statusController;
35
+ }
36
+ /** Read-only view of the currently queued attachments. Useful for callers
37
+ * that need to render attachment footnotes alongside the user message. */
38
+ attachments() {
39
+ return this.pendingImages;
40
+ }
41
+ /**
42
+ * Drain the queue, returning the attachments that were pending at submit
43
+ * time. The caller forwards `.attachment` payloads to the runner; the
44
+ * controller resets so the next turn starts at `[Image #1]`.
45
+ */
46
+ consume() {
47
+ const drained = this.pendingImages;
48
+ this.pendingImages = [];
49
+ this.nextImageId = 1;
50
+ this.statusController.setPendingImageCount(0);
51
+ return drained;
52
+ }
53
+ /** Drop all queued attachments. No-op when the queue is already empty so
54
+ * `/clear-images` does not redundantly repaint the hint. */
55
+ clearPendingImages() {
56
+ if (this.pendingImages.length === 0)
57
+ return;
58
+ this.pendingImages = [];
59
+ this.nextImageId = 1;
60
+ this.statusController.setPendingImageCount(0);
61
+ }
62
+ /**
63
+ * Handle a bracketed-paste event from the textarea. Decides synchronously
64
+ * whether the payload is image bytes, a file path, an empty trigger that
65
+ * warrants a clipboard probe, or plain text that should fall through to
66
+ * the textarea's default insert path.
67
+ */
68
+ async handlePasteEvent(event) {
69
+ const metadata = event.metadata;
70
+ const sniffed = sniffImageMimeType(event.bytes);
71
+ const inferredMime = metadata?.mimeType && metadata.mimeType.startsWith("image/") ? metadata.mimeType : sniffed;
72
+ // Synchronous fast paths — the paste payload itself is enough to decide.
73
+ if (inferredMime) {
74
+ event.preventDefault();
75
+ await this.attachPastedImageBytes(event.bytes, inferredMime);
76
+ return;
77
+ }
78
+ if (metadata?.kind === "binary") {
79
+ // Non-image binary paste — we cannot meaningfully forward it, but the
80
+ // terminal already swallowed the keystroke, so suppress the default
81
+ // text-insert path that would otherwise garble the prompt.
82
+ event.preventDefault();
83
+ this.appendBlock("[paste]", "Unsupported binary clipboard contents (only PNG/JPEG/GIF/WebP).", COLORS.system);
84
+ return;
85
+ }
86
+ // Text-shaped paste. Three sub-cases, ordered cheapest first so common
87
+ // text pastes never wait on the macOS Swift clipboard probe:
88
+ //
89
+ // 1. The text resolves to an image file path (Finder/Files drag-paste).
90
+ // 2. The terminal forwarded an empty payload but the OS clipboard
91
+ // may carry an image promise (e.g. Figma "Copy as PNG", screenshot,
92
+ // browser image copy that bracketed-paste cannot represent).
93
+ // 3. Plain text — just insert it.
94
+ //
95
+ // Sub-cases 1 and 3 are fully synchronous after the path heuristic, so
96
+ // the buffer paints immediately. Only sub-case 2 spawns the Swift
97
+ // probe, and only when there is literally no text to insert anyway.
98
+ const originalText = decodePasteBytes(event.bytes);
99
+ const candidate = looksLikeImageFilePath(originalText);
100
+ if (candidate) {
101
+ // Path-shaped paste: suppress the default insert so we can swap in
102
+ // the [Image #N] placeholder once load resolves.
103
+ event.preventDefault();
104
+ try {
105
+ const pending = await loadImageFromPath({
106
+ cwd: this.workDir,
107
+ rawPath: candidate,
108
+ id: this.nextImageId,
109
+ });
110
+ this.nextImageId += 1;
111
+ this.pendingImages.push(pending);
112
+ this.inputField.insertText(pending.label);
113
+ this.appendBlock("[paste]", `attached ${pending.label} from ${pending.path}`, COLORS.system);
114
+ this.statusController.setPendingImageCount(this.pendingImages.length);
115
+ }
116
+ catch (error) {
117
+ // The clipboard looked like an image path but we could not load
118
+ // it — surface why and restore the original text so the user can
119
+ // edit it manually instead of losing what they pasted.
120
+ this.appendBlock("[paste]", `looked like an image path but could not attach ${candidate}: ${error instanceof Error ? error.message : String(error)}`, COLORS.system);
121
+ if (originalText.length > 0)
122
+ this.inputField.insertText(originalText);
123
+ }
124
+ return;
125
+ }
126
+ if (originalText.length === 0) {
127
+ // No text payload — the terminal had nothing to forward but the OS
128
+ // clipboard may still carry an image promise. Suppress the default
129
+ // (which would do nothing anyway) and run the slow probe.
130
+ event.preventDefault();
131
+ const clipboardImage = await tryReadClipboardImage();
132
+ if (clipboardImage) {
133
+ await this.attachPastedImageBytes(clipboardImage.bytes, clipboardImage.mimeType);
134
+ }
135
+ return;
136
+ }
137
+ // Plain text paste — fall through to the InputRenderable's default
138
+ // insert path, which paints synchronously. Users whose intended image
139
+ // arrived as text-shaped bytes (e.g. Figma) can still trigger an
140
+ // explicit clipboard probe via the `/paste` slash command.
141
+ }
142
+ /**
143
+ * Manual clipboard probe. Read the OS clipboard for an image right now and
144
+ * attach it if found; otherwise fall back to a text probe so a Cmd+V/Ctrl+V
145
+ * keystroke that ate a legitimate text paste does not silently swallow it.
146
+ * The `source` distinguishes the keystroke entry point from the `/paste`
147
+ * slash command so we only emit the "no readable image" diagnostic for
148
+ * explicit user intent.
149
+ */
150
+ async triggerClipboardProbe(source) {
151
+ try {
152
+ const clipboardImage = await tryReadClipboardImage();
153
+ if (clipboardImage) {
154
+ await this.attachPastedImageBytes(clipboardImage.bytes, clipboardImage.mimeType);
155
+ return;
156
+ }
157
+ // No image on the clipboard. The keystroke path may have eaten a
158
+ // legitimate text paste, so fall back to a text probe so users do not
159
+ // lose what they were trying to paste.
160
+ const text = await tryReadClipboardText();
161
+ if (text) {
162
+ this.inputField.insertText(text);
163
+ return;
164
+ }
165
+ if (source === "slash") {
166
+ // Surface the actual clipboard UTI list when a /paste probe comes
167
+ // up empty — lets users see what their source app actually put
168
+ // there so the failure stops being mysterious.
169
+ const types = await describeMacClipboardTypes();
170
+ const detail = types
171
+ ? ` — clipboard types: ${types}`
172
+ : " — (could not query clipboard types; clipboard may be empty)";
173
+ this.appendBlock("[paste]", `clipboard had no readable image or text${detail}`, COLORS.system);
174
+ }
175
+ }
176
+ catch (error) {
177
+ this.appendBlock("[paste]", `clipboard probe failed: ${error instanceof Error ? error.message : String(error)}`, COLORS.error);
178
+ }
179
+ }
180
+ /**
181
+ * Attach an image from disk by path. Drives the `/image <path>` slash
182
+ * command. Inserts the `[Image #N]` placeholder back into the (likely now
183
+ * empty) input so the user can keep typing their prompt with the image
184
+ * already attached.
185
+ */
186
+ async attachImageFromPath(rawPath) {
187
+ try {
188
+ const pending = await loadImageFromPath({
189
+ cwd: this.workDir,
190
+ rawPath,
191
+ id: this.nextImageId,
192
+ });
193
+ this.nextImageId += 1;
194
+ this.pendingImages.push(pending);
195
+ this.inputField.insertText(pending.label);
196
+ this.appendBlock("[paste]", `attached ${pending.label} from ${pending.path}`, COLORS.system);
197
+ this.statusController.setPendingImageCount(this.pendingImages.length);
198
+ }
199
+ catch (error) {
200
+ this.appendBlock("[paste]", error instanceof Error ? error.message : String(error), COLORS.error);
201
+ }
202
+ }
203
+ /**
204
+ * Persist raw image bytes (from a bracketed-paste event or clipboard probe)
205
+ * under the session cache and surface the resulting `[Image #N]`
206
+ * placeholder in the prompt buffer.
207
+ */
208
+ async attachPastedImageBytes(bytes, mimeType) {
209
+ try {
210
+ const pending = await persistPastedImage({
211
+ sessionId: this.sessionId,
212
+ id: this.nextImageId,
213
+ bytes,
214
+ mimeType,
215
+ });
216
+ this.nextImageId += 1;
217
+ this.pendingImages.push(pending);
218
+ this.inputField.insertText(pending.label);
219
+ this.appendBlock("[paste]", `attached ${pending.label} (${mimeType}, ${formatBytes(bytes.length)})`, COLORS.system);
220
+ this.statusController.setPendingImageCount(this.pendingImages.length);
221
+ }
222
+ catch (error) {
223
+ this.appendBlock("[paste]", error instanceof Error ? error.message : String(error), COLORS.error);
224
+ }
225
+ }
226
+ }
227
+ function formatBytes(n) {
228
+ if (n < 1024)
229
+ return `${n} B`;
230
+ if (n < 1024 * 1024)
231
+ return `${(n / 1024).toFixed(1)} KB`;
232
+ return `${(n / (1024 * 1024)).toFixed(1)} MB`;
233
+ }
234
+ //# sourceMappingURL=paste-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paste-controller.js","sourceRoot":"","sources":["../../../src/tui/paste-controller.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,111 @@
1
+ import { type BoxRenderable, type KeyEvent, type TextareaRenderable, type TextRenderable } from "@opentui/core";
2
+ import type { Session } from "../session/session.js";
3
+ import type { TranscriptEntryKind } from "./transcript-log.js";
4
+ import type { TurnPromptImage, TurnQuestion } from "../types/protocol.js";
5
+ export interface QuestionPickerOptions {
6
+ /** Bordered panel that wraps the title + option rows; toggled visible
7
+ * alongside the row pool. */
8
+ questionPanel: BoxRenderable;
9
+ /** Single text row used as the panel header; rewritten on every render
10
+ * with the active question text and (i/N) progress. */
11
+ questionTitle: TextRenderable;
12
+ /** Fixed-size pool of selectable rows. The picker shows/hides entries
13
+ * from this pool rather than mounting fresh renderables per question
14
+ * so layout heights stay stable across questions. */
15
+ questionRows: TextRenderable[];
16
+ /** Composer textarea. Read for `plainText.length` so the picker can
17
+ * defer Space/Left/Right bindings to typing when the user is mid-prompt. */
18
+ inputField: TextareaRenderable;
19
+ /** Session that receives the assembled answer payload via `.answer(...)`
20
+ * once the user finishes the picker. */
21
+ session: Session;
22
+ /** Notify the host that the picker is about to swallow an Escape so the
23
+ * global escape-to-exit handler suppresses the next Escape it sees and
24
+ * doesn't double-handle the keystroke. */
25
+ onEscapeClose: () => void;
26
+ /** Append a bordered transcript block (label + body). Used for the
27
+ * `you:` echo when the user confirms an answer, mirroring how free-form
28
+ * prompts render. */
29
+ appendBlock: (label: string | null, body: string, color: string) => void;
30
+ /** Mirror committed answers into the in-memory transcript log so /copy
31
+ * and resume snapshots include them in order. */
32
+ recordTranscriptEntry: (kind: TranscriptEntryKind, text: string) => void;
33
+ /** Surface a dispatch failure (rejected `session.answer` promise) the
34
+ * same way the rest of runTui does. */
35
+ reportError: (error: unknown) => void;
36
+ /** Bridge to the status controller so the picker can flip the working
37
+ * indicator on when an answer kicks off a new turn. */
38
+ markRunning: () => void;
39
+ /** Asked before `markRunning()` so the picker mirrors the host's
40
+ * conditional "only mark when currently idle" behaviour. */
41
+ isRunning: () => boolean;
42
+ }
43
+ /**
44
+ * Owns the question picker: an ask terminal hands it a list of
45
+ * `TurnQuestion`s and the picker walks the user through them, accumulating
46
+ * selected option labels until the user either finishes the last question
47
+ * (dispatches an `answer`), types a free-form prompt over the top
48
+ * (`flushWithMessage`), or escapes out (host suppresses next escape so the
49
+ * TUI doesn't also exit).
50
+ *
51
+ * Single-select questions live-record the highlighted option as the answer
52
+ * so arrow navigation alone is enough; multi-select keeps the highlight
53
+ * separate from a checked-set toggled via Space/Enter and grows a synthetic
54
+ * "Done" row that advances to the next question.
55
+ */
56
+ export declare class QuestionPicker {
57
+ private readonly opts;
58
+ private pendingQuestions;
59
+ private activeIndex;
60
+ private optionSelectedIndex;
61
+ private multiSelectChecked;
62
+ private accumulatedAnswers;
63
+ constructor(opts: QuestionPickerOptions);
64
+ isOpen(): boolean;
65
+ show(questions: TurnQuestion[]): void;
66
+ hide(): void;
67
+ /**
68
+ * Confirm whatever is currently highlighted. Multi-select on a regular row
69
+ * toggles; multi-select on the Done row advances. Single-select always
70
+ * advances (highlight = answer is already live-recorded by Up/Down). No-op
71
+ * when nothing is highlighted yet so the user is forced to make an
72
+ * explicit choice (or skip via Right-arrow).
73
+ */
74
+ confirmSelection(): boolean;
75
+ /**
76
+ * Dispatch the partially-collected answers together with a free-form
77
+ * `message` (and any pending image attachments) as a single
78
+ * `session.answer({ ..., message, images })` turn, then hide the picker.
79
+ * Used when the user types over the top of an open picker — without this
80
+ * the partial selections would be discarded when the prompt fires.
81
+ */
82
+ flushWithMessage(message: string, images: TurnPromptImage[]): void;
83
+ /**
84
+ * Handle a keystroke while the picker is open. Returns `true` if the key
85
+ * was claimed (and `key.preventDefault()` already called); the caller
86
+ * should stop further dispatch in that case. Returns `false` when the
87
+ * picker is closed or the key isn't one this controller handles.
88
+ */
89
+ handleKey(key: KeyEvent): boolean;
90
+ private activeQuestion;
91
+ /**
92
+ * Total navigable rows for the active question. The Up/Down handler clamps
93
+ * navigation to what is actually rendered on screen so a user cannot land
94
+ * the highlight on a row they cannot see.
95
+ */
96
+ private activeRowCount;
97
+ /**
98
+ * Row index of the synthetic Done row when the active question is
99
+ * multi-select; `undefined` otherwise so callers don't compare against a
100
+ * sentinel value. The Done row sits one past the last visible option,
101
+ * clamped to the same limit `render()` uses.
102
+ */
103
+ private activeQuestionDoneIndex;
104
+ private render;
105
+ private moveActiveHighlight;
106
+ private toggleActiveMultiSelectOption;
107
+ private navigateActive;
108
+ private describeAnswerLabels;
109
+ private advanceOrSubmit;
110
+ }
111
+ //# sourceMappingURL=question-picker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"question-picker.d.ts","sourceRoot":"","sources":["../../../src/tui/question-picker.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAElB,KAAK,QAAQ,EAEb,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACpB,MAAM,eAAe,CAAC;AASvB,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAErD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAI1E,MAAM,WAAW,qBAAqB;IACpC;kCAC8B;IAC9B,aAAa,EAAE,aAAa,CAAC;IAC7B;4DACwD;IACxD,aAAa,EAAE,cAAc,CAAC;IAC9B;;0DAEsD;IACtD,YAAY,EAAE,cAAc,EAAE,CAAC;IAC/B;iFAC6E;IAC7E,UAAU,EAAE,kBAAkB,CAAC;IAC/B;6CACyC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB;;+CAE2C;IAC3C,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B;;0BAEsB;IACtB,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE;sDACkD;IAClD,qBAAqB,EAAE,CAAC,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACzE;4CACwC;IACxC,WAAW,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC;4DACwD;IACxD,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB;iEAC6D;IAC7D,SAAS,EAAE,MAAM,OAAO,CAAC;CAC1B;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,cAAc;IAiBb,OAAO,CAAC,QAAQ,CAAC,IAAI;IAhBjC,OAAO,CAAC,gBAAgB,CAAsB;IAC9C,OAAO,CAAC,WAAW,CAAK;IAKxB,OAAO,CAAC,mBAAmB,CAAgB;IAI3C,OAAO,CAAC,kBAAkB,CAAqB;IAI/C,OAAO,CAAC,kBAAkB,CAAgC;gBAE7B,IAAI,EAAE,qBAAqB;IAExD,MAAM,IAAI,OAAO;IAKjB,IAAI,CAAC,SAAS,EAAE,YAAY,EAAE,GAAG,IAAI;IASrC,IAAI,IAAI,IAAI;IAaZ;;;;;;OAMG;IACH,gBAAgB,IAAI,OAAO;IAW3B;;;;;;OAMG;IACH,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,EAAE,GAAG,IAAI;IAclE;;;;;OAKG;IACH,SAAS,CAAC,GAAG,EAAE,QAAQ,GAAG,OAAO;IA8CjC,OAAO,CAAC,cAAc;IAItB;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAQtB;;;;;OAKG;IACH,OAAO,CAAC,uBAAuB;IAO/B,OAAO,CAAC,MAAM;IAyDd,OAAO,CAAC,mBAAmB;IAsB3B,OAAO,CAAC,6BAA6B;IAiBrC,OAAO,CAAC,cAAc;IAoBtB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,eAAe;CA8BxB"}