@sagmans/dsh-tui 0.6.0 → 0.7.0

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 (250) hide show
  1. package/.agents/skills/dsh-tui-dogfood/SKILL.md +83 -0
  2. package/.agents/skills/dsh-tui-dogfood/references/home-state.md +78 -0
  3. package/.agents/skills/dsh-tui-dogfood/scripts/clone-links.mjs +133 -0
  4. package/.agents/skills/dsh-tui-dogfood/scripts/run-plugin-from-worktree.sh +417 -0
  5. package/.agents/skills/dsh-tui-update-models/SKILL.md +121 -0
  6. package/.agents/skills/dsh-tui-update-models/references/model-wiring.md +162 -0
  7. package/.agents/skills/dsh-tui-update-models/scripts/dump-model-catalog.mjs +178 -0
  8. package/README.md +99 -5
  9. package/lib/agent/present.d.ts.map +1 -1
  10. package/lib/agent/present.js +2 -1
  11. package/lib/agent/present.js.map +1 -1
  12. package/lib/cards/composition.d.ts +68 -0
  13. package/lib/cards/composition.d.ts.map +1 -0
  14. package/lib/cards/composition.js +117 -0
  15. package/lib/cards/composition.js.map +1 -0
  16. package/lib/cards/presenter.d.ts +23 -0
  17. package/lib/cards/presenter.d.ts.map +1 -0
  18. package/lib/cards/presenter.js +394 -0
  19. package/lib/cards/presenter.js.map +1 -0
  20. package/lib/cards/preview.d.ts +42 -0
  21. package/lib/cards/preview.d.ts.map +1 -0
  22. package/lib/cards/preview.js +51 -0
  23. package/lib/cards/preview.js.map +1 -0
  24. package/lib/cards.d.ts +12 -120
  25. package/lib/cards.d.ts.map +1 -1
  26. package/lib/cards.js +4 -556
  27. package/lib/cards.js.map +1 -1
  28. package/lib/compat/section.d.ts +29 -0
  29. package/lib/compat/section.d.ts.map +1 -0
  30. package/lib/compat/section.js +82 -0
  31. package/lib/compat/section.js.map +1 -0
  32. package/lib/config.d.ts +27 -2
  33. package/lib/config.d.ts.map +1 -1
  34. package/lib/config.js +61 -1
  35. package/lib/config.js.map +1 -1
  36. package/lib/contracts.d.ts +28 -0
  37. package/lib/contracts.d.ts.map +1 -1
  38. package/lib/fold-cursor.d.ts +8 -0
  39. package/lib/fold-cursor.d.ts.map +1 -1
  40. package/lib/fold-cursor.js +18 -0
  41. package/lib/fold-cursor.js.map +1 -1
  42. package/lib/gates/question-card.d.ts +77 -0
  43. package/lib/gates/question-card.d.ts.map +1 -0
  44. package/lib/gates/question-card.js +113 -0
  45. package/lib/gates/question-card.js.map +1 -0
  46. package/lib/gates/questions.d.ts +127 -0
  47. package/lib/gates/questions.d.ts.map +1 -0
  48. package/lib/gates/questions.js +418 -0
  49. package/lib/gates/questions.js.map +1 -0
  50. package/lib/gates.d.ts +1 -113
  51. package/lib/gates.d.ts.map +1 -1
  52. package/lib/gates.js +1 -484
  53. package/lib/gates.js.map +1 -1
  54. package/lib/index.d.ts +1 -0
  55. package/lib/index.d.ts.map +1 -1
  56. package/lib/index.js +343 -2602
  57. package/lib/index.js.map +1 -1
  58. package/lib/input/action-catalog.d.ts +74 -0
  59. package/lib/input/action-catalog.d.ts.map +1 -0
  60. package/lib/input/action-catalog.js +137 -0
  61. package/lib/input/action-catalog.js.map +1 -0
  62. package/lib/input/actions.d.ts +5 -81
  63. package/lib/input/actions.d.ts.map +1 -1
  64. package/lib/input/actions.js +4 -625
  65. package/lib/input/actions.js.map +1 -1
  66. package/lib/input/completion.d.ts +1 -1
  67. package/lib/input/completion.d.ts.map +1 -1
  68. package/lib/input/completion.js +2 -1
  69. package/lib/input/completion.js.map +1 -1
  70. package/lib/input/file-index.d.ts +40 -0
  71. package/lib/input/file-index.d.ts.map +1 -0
  72. package/lib/input/file-index.js +141 -0
  73. package/lib/input/file-index.js.map +1 -0
  74. package/lib/input/file-search.d.ts +1 -38
  75. package/lib/input/file-search.d.ts.map +1 -1
  76. package/lib/input/file-search.js +1 -487
  77. package/lib/input/file-search.js.map +1 -1
  78. package/lib/input/key-press.d.ts +57 -0
  79. package/lib/input/key-press.d.ts.map +1 -0
  80. package/lib/input/key-press.js +218 -0
  81. package/lib/input/key-press.js.map +1 -0
  82. package/lib/input/keymap-conflicts.d.ts +59 -0
  83. package/lib/input/keymap-conflicts.d.ts.map +1 -0
  84. package/lib/input/keymap-conflicts.js +292 -0
  85. package/lib/input/keymap-conflicts.js.map +1 -0
  86. package/lib/input/keymap-settings.js +1 -1
  87. package/lib/input/keymap-settings.js.map +1 -1
  88. package/lib/input/keymap.d.ts.map +1 -1
  89. package/lib/input/keymap.js +3 -1
  90. package/lib/input/keymap.js.map +1 -1
  91. package/lib/input/workspace-files.d.ts +10 -0
  92. package/lib/input/workspace-files.d.ts.map +1 -0
  93. package/lib/input/workspace-files.js +359 -0
  94. package/lib/input/workspace-files.js.map +1 -0
  95. package/lib/install-skills.d.ts +20 -0
  96. package/lib/install-skills.d.ts.map +1 -0
  97. package/lib/install-skills.js +198 -0
  98. package/lib/install-skills.js.map +1 -0
  99. package/lib/keys-command.d.ts +2 -1
  100. package/lib/keys-command.d.ts.map +1 -1
  101. package/lib/keys-command.js +2 -1
  102. package/lib/keys-command.js.map +1 -1
  103. package/lib/model-list.d.ts +37 -0
  104. package/lib/model-list.d.ts.map +1 -0
  105. package/lib/model-list.js +60 -0
  106. package/lib/model-list.js.map +1 -0
  107. package/lib/startup.d.ts +8 -0
  108. package/lib/startup.d.ts.map +1 -1
  109. package/lib/startup.js +120 -24
  110. package/lib/startup.js.map +1 -1
  111. package/lib/subagents.d.ts +7 -3
  112. package/lib/subagents.d.ts.map +1 -1
  113. package/lib/subagents.js +28 -4
  114. package/lib/subagents.js.map +1 -1
  115. package/lib/surface/appearance-preferences.d.ts +26 -0
  116. package/lib/surface/appearance-preferences.d.ts.map +1 -0
  117. package/lib/surface/appearance-preferences.js +139 -0
  118. package/lib/surface/appearance-preferences.js.map +1 -0
  119. package/lib/surface/appearance.d.ts +87 -0
  120. package/lib/surface/appearance.d.ts.map +1 -0
  121. package/lib/surface/appearance.js +353 -0
  122. package/lib/surface/appearance.js.map +1 -0
  123. package/lib/surface/background-work.d.ts +49 -0
  124. package/lib/surface/background-work.d.ts.map +1 -0
  125. package/lib/surface/background-work.js +146 -0
  126. package/lib/surface/background-work.js.map +1 -0
  127. package/lib/surface/commands.d.ts +80 -0
  128. package/lib/surface/commands.d.ts.map +1 -0
  129. package/lib/surface/commands.js +364 -0
  130. package/lib/surface/commands.js.map +1 -0
  131. package/lib/surface/modal-input.d.ts +58 -0
  132. package/lib/surface/modal-input.d.ts.map +1 -0
  133. package/lib/surface/modal-input.js +237 -0
  134. package/lib/surface/modal-input.js.map +1 -0
  135. package/lib/surface/model-choice.d.ts +54 -0
  136. package/lib/surface/model-choice.d.ts.map +1 -0
  137. package/lib/surface/model-choice.js +325 -0
  138. package/lib/surface/model-choice.js.map +1 -0
  139. package/lib/surface/preset-choice.d.ts +78 -0
  140. package/lib/surface/preset-choice.d.ts.map +1 -0
  141. package/lib/surface/preset-choice.js +163 -0
  142. package/lib/surface/preset-choice.js.map +1 -0
  143. package/lib/surface/prompt-input.d.ts +97 -0
  144. package/lib/surface/prompt-input.d.ts.map +1 -0
  145. package/lib/surface/prompt-input.js +215 -0
  146. package/lib/surface/prompt-input.js.map +1 -0
  147. package/lib/surface/prompt-memory.d.ts +76 -0
  148. package/lib/surface/prompt-memory.d.ts.map +1 -0
  149. package/lib/surface/prompt-memory.js +167 -0
  150. package/lib/surface/prompt-memory.js.map +1 -0
  151. package/lib/surface/session-lifecycle.d.ts +120 -0
  152. package/lib/surface/session-lifecycle.d.ts.map +1 -0
  153. package/lib/surface/session-lifecycle.js +321 -0
  154. package/lib/surface/session-lifecycle.js.map +1 -0
  155. package/lib/surface/session-picker.d.ts +39 -0
  156. package/lib/surface/session-picker.d.ts.map +1 -0
  157. package/lib/surface/session-picker.js +107 -0
  158. package/lib/surface/session-picker.js.map +1 -0
  159. package/lib/surface/session-view.d.ts +86 -0
  160. package/lib/surface/session-view.d.ts.map +1 -0
  161. package/lib/surface/session-view.js +215 -0
  162. package/lib/surface/session-view.js.map +1 -0
  163. package/lib/surface/staged-turns.d.ts +81 -0
  164. package/lib/surface/staged-turns.d.ts.map +1 -0
  165. package/lib/surface/staged-turns.js +206 -0
  166. package/lib/surface/staged-turns.js.map +1 -0
  167. package/lib/surface/terminal-lifecycle.d.ts +76 -0
  168. package/lib/surface/terminal-lifecycle.d.ts.map +1 -0
  169. package/lib/surface/terminal-lifecycle.js +222 -0
  170. package/lib/surface/terminal-lifecycle.js.map +1 -0
  171. package/lib/terminal-text/scan.d.ts +44 -0
  172. package/lib/terminal-text/scan.d.ts.map +1 -0
  173. package/lib/terminal-text/scan.js +195 -0
  174. package/lib/terminal-text/scan.js.map +1 -0
  175. package/lib/terminal-text/sgr.d.ts +32 -0
  176. package/lib/terminal-text/sgr.d.ts.map +1 -0
  177. package/lib/terminal-text/sgr.js +203 -0
  178. package/lib/terminal-text/sgr.js.map +1 -0
  179. package/lib/terminal-text.d.ts +1 -1
  180. package/lib/terminal-text.d.ts.map +1 -1
  181. package/lib/terminal-text.js +2 -379
  182. package/lib/terminal-text.js.map +1 -1
  183. package/lib/theme-command.js +2 -1
  184. package/lib/theme-command.js.map +1 -1
  185. package/lib/theme-defaults.d.ts +64 -0
  186. package/lib/theme-defaults.d.ts.map +1 -0
  187. package/lib/theme-defaults.js +248 -0
  188. package/lib/theme-defaults.js.map +1 -0
  189. package/lib/theme-resolver.d.ts +36 -0
  190. package/lib/theme-resolver.d.ts.map +1 -0
  191. package/lib/theme-resolver.js +112 -0
  192. package/lib/theme-resolver.js.map +1 -0
  193. package/lib/theme-schema.d.ts.map +1 -1
  194. package/lib/theme-schema.js +3 -2
  195. package/lib/theme-schema.js.map +1 -1
  196. package/lib/theme-settings.d.ts +5 -7
  197. package/lib/theme-settings.d.ts.map +1 -1
  198. package/lib/theme-settings.js +27 -19
  199. package/lib/theme-settings.js.map +1 -1
  200. package/lib/theme-tokens.d.ts +0 -89
  201. package/lib/theme-tokens.d.ts.map +1 -1
  202. package/lib/theme-tokens.js +0 -356
  203. package/lib/theme-tokens.js.map +1 -1
  204. package/lib/theme.d.ts +1 -1
  205. package/lib/theme.d.ts.map +1 -1
  206. package/lib/theme.js +2 -1
  207. package/lib/theme.js.map +1 -1
  208. package/lib/tool-display.d.ts.map +1 -1
  209. package/lib/tool-display.js +2 -1
  210. package/lib/tool-display.js.map +1 -1
  211. package/lib/transcript/message-content.d.ts +21 -0
  212. package/lib/transcript/message-content.d.ts.map +1 -0
  213. package/lib/transcript/message-content.js +61 -0
  214. package/lib/transcript/message-content.js.map +1 -0
  215. package/lib/transcript/tool-calls.d.ts +106 -0
  216. package/lib/transcript/tool-calls.d.ts.map +1 -0
  217. package/lib/transcript/tool-calls.js +254 -0
  218. package/lib/transcript/tool-calls.js.map +1 -0
  219. package/lib/transcript.d.ts +14 -55
  220. package/lib/transcript.d.ts.map +1 -1
  221. package/lib/transcript.js +31 -253
  222. package/lib/transcript.js.map +1 -1
  223. package/lib/ui/dock.d.ts +14 -2
  224. package/lib/ui/dock.d.ts.map +1 -1
  225. package/lib/ui/dock.js +52 -10
  226. package/lib/ui/dock.js.map +1 -1
  227. package/lib/ui/editor.d.ts.map +1 -1
  228. package/lib/ui/editor.js +2 -1
  229. package/lib/ui/editor.js.map +1 -1
  230. package/lib/ui/keymap-picker.d.ts +2 -1
  231. package/lib/ui/keymap-picker.d.ts.map +1 -1
  232. package/lib/ui/keymap-picker.js +1 -0
  233. package/lib/ui/keymap-picker.js.map +1 -1
  234. package/lib/ui/view/gate-card.d.ts +21 -0
  235. package/lib/ui/view/gate-card.d.ts.map +1 -0
  236. package/lib/ui/view/gate-card.js +104 -0
  237. package/lib/ui/view/gate-card.js.map +1 -0
  238. package/lib/ui/view/tool-card.d.ts +138 -0
  239. package/lib/ui/view/tool-card.d.ts.map +1 -0
  240. package/lib/ui/view/tool-card.js +413 -0
  241. package/lib/ui/view/tool-card.js.map +1 -0
  242. package/lib/ui/view/transcript-message.d.ts +62 -0
  243. package/lib/ui/view/transcript-message.d.ts.map +1 -0
  244. package/lib/ui/view/transcript-message.js +158 -0
  245. package/lib/ui/view/transcript-message.js.map +1 -0
  246. package/lib/ui/view.d.ts +80 -220
  247. package/lib/ui/view.d.ts.map +1 -1
  248. package/lib/ui/view.js +81 -675
  249. package/lib/ui/view.js.map +1 -1
  250. package/package.json +3 -1
package/lib/gates.d.ts CHANGED
@@ -70,6 +70,7 @@ export interface GateCustomRow {
70
70
  /** Whether a custom answer is being formed or already holds text. */
71
71
  readonly selected: boolean;
72
72
  }
73
+ export declare function lines(value: string): string[];
73
74
  /**
74
75
  * One pending approval.
75
76
  *
@@ -110,119 +111,6 @@ export interface GateAnswer {
110
111
  readonly selected: readonly string[];
111
112
  readonly custom?: string;
112
113
  }
113
- /**
114
- * Read the seam's request into the gate's own vocabulary.
115
- *
116
- * A question without options is answered by typing, which is why it becomes an
117
- * option-less gate rather than a rejected request: the model asked for text.
118
- */
119
- export declare function toGateQuestions(request: unknown): GateQuestion[];
120
114
  /** The number the free-text row answers for. It sits below the window, so drawn numbering runs 1..n then 0. */
121
115
  export declare const CUSTOM_ROW_NUMBER = 0;
122
- /**
123
- * A pending batch of questions.
124
- *
125
- * Answers are collected per question and returned in the seam's shape. An
126
- * escape skips the current question with an empty selection rather than
127
- * aborting the batch, so a human who cannot answer one question still returns
128
- * everything else they decided.
129
- */
130
- export declare class QuestionGate {
131
- private readonly questions;
132
- /** The editor every typed answer is written in, whichever row asks for it. */
133
- private readonly input;
134
- /** The keys in force, read per press so a settings edit lands on the next key. */
135
- private readonly keys;
136
- private index;
137
- private cursor;
138
- private typed;
139
- /** Whether the cursor is on the free-text row. */
140
- private atCustom;
141
- /**
142
- * The row an escape from the free-text row returns to.
143
- *
144
- * It is the row the reader last acted on — walked to, picked by its number, or
145
- * toggled — because that is the option they were looking at when they decided
146
- * to answer freely, and it is not always the row the cursor still stands on: a
147
- * digit picks by number without walking anywhere.
148
- */
149
- private customFrom;
150
- private readonly chosen;
151
- private readonly custom;
152
- private finished;
153
- constructor(questions: readonly GateQuestion[],
154
- /** The editor every typed answer is written in, whichever row asks for it. */
155
- input: GateInput,
156
- /** The keys in force, read per press so a settings edit lands on the next key. */
157
- keys: () => Keymap);
158
- /**
159
- * Empty the editor for the question now under the cursor.
160
- *
161
- * The editor belongs to the surface and outlives one question, so an answer
162
- * must never cross from the last question into the next — or from a gate that
163
- * was abandoned into the one that follows it. Its mode follows the question
164
- * for the same reason: a credential must not be shown because the last
165
- * question was not one.
166
- */
167
- private resetInput;
168
- get resolved(): boolean;
169
- /**
170
- * Stop a question whose caller is gone.
171
- *
172
- * WHY: an aborted call has no reader for the answer, and a live gate would
173
- * otherwise keep the keyboard — and the borrowed editor — until someone
174
- * typed into a call that had already ended.
175
- */
176
- cancel(): void;
177
- private get current();
178
- /**
179
- * Options the typed filter leaves, in the order they were listed.
180
- *
181
- * The order is not the score order a picker uses: a digit answers by
182
- * position, and re-ranking rows would move the number the reader just read.
183
- */
184
- private matched;
185
- /**
186
- * The rows the keys and the card agree on: the filter, then a window around
187
- * the cursor, because a list as long as a provider catalog would otherwise
188
- * push the editor off the screen.
189
- */
190
- private windowed;
191
- /** The row under the cursor, as the filter leaves it. */
192
- private currentRow;
193
- /** Take or drop one source option, by single-select replacement or multi-select toggle. */
194
- private pick;
195
- /**
196
- * Whether a keystroke is answer text rather than a filter.
197
- *
198
- * A question without options has no list to filter, and the free-text row is
199
- * where a question with options collects an answer the model did not list.
200
- */
201
- private typingAnswer;
202
- /** Add text to the filter, or to the answer the current row collects. */
203
- private absorb;
204
- /** Apply one key press; returns the batch answer the first time it completes. */
205
- handleKey(data: string): GateAnswer[] | undefined;
206
- /**
207
- * One key press while the free-text row holds the cursor.
208
- *
209
- * Walking back to the list keeps whatever was typed, so a reader who changes
210
- * their mind about answering freely has not lost the text yet. An escape
211
- * leaves the row the same way, because a question skipped by accident is a
212
- * question the reader has to answer again: from the list, where every other
213
- * question is skipped, an escape skips this one too.
214
- */
215
- private handleCustomKey;
216
- /** Take the answer the reader confirmed, then leave the question. */
217
- private confirm;
218
- /**
219
- * Leave the current question for the next one.
220
- *
221
- * Typed text belongs to the question being left, so it never crosses into the
222
- * next one — which is why a skipped question reports no typed answer at all.
223
- */
224
- private advance;
225
- private result;
226
- card(): GateCard;
227
- }
228
116
  //# sourceMappingURL=gates.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"gates.d.ts","sourceRoot":"","sources":["../src/gates.ts"],"names":[],"mappings":"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"}
package/lib/gates.js CHANGED
@@ -1,8 +1,5 @@
1
- import { matchesKey } from '@earendil-works/pi-tui';
2
1
  import { actionLabel, keyName, keysFor, matchesAction } from "./input/actions.js";
3
- import { pastedText } from "./input.js";
4
- import { matchScore } from "./input/match.js";
5
- function lines(value) {
2
+ export function lines(value) {
6
3
  return value.split('\n').filter(line => line.trim() !== '');
7
4
  }
8
5
  /**
@@ -59,486 +56,6 @@ export class ApprovalGate {
59
56
  };
60
57
  }
61
58
  }
62
- function asRecord(value) {
63
- return typeof value === 'object' && value !== null ? value : undefined;
64
- }
65
- /**
66
- * Read the seam's request into the gate's own vocabulary.
67
- *
68
- * A question without options is answered by typing, which is why it becomes an
69
- * option-less gate rather than a rejected request: the model asked for text.
70
- */
71
- export function toGateQuestions(request) {
72
- const record = asRecord(request);
73
- const raw = Array.isArray(record?.questions) ? record.questions : [];
74
- const questions = [];
75
- for (const item of raw) {
76
- const entry = asRecord(item);
77
- if (entry === undefined || typeof entry.id !== 'string')
78
- continue;
79
- const options = (Array.isArray(entry.options) ? entry.options : []).flatMap(option => {
80
- const candidate = asRecord(option);
81
- if (candidate === undefined || typeof candidate.label !== 'string')
82
- return [];
83
- return [{
84
- label: candidate.label,
85
- description: typeof candidate.description === 'string' ? candidate.description : undefined,
86
- }];
87
- });
88
- questions.push({
89
- id: entry.id,
90
- question: typeof entry.question === 'string' ? entry.question : 'question',
91
- header: typeof entry.header === 'string' ? entry.header : undefined,
92
- detail: typeof entry.detail === 'string' ? entry.detail : undefined,
93
- options,
94
- multiSelect: entry.multiSelect === true,
95
- });
96
- }
97
- return questions;
98
- }
99
- /** How many option rows a question shows at once, so a catalog-sized list leaves the editor in view. */
100
- const QUESTION_WINDOW = 12;
101
59
  /** The number the free-text row answers for. It sits below the window, so drawn numbering runs 1..n then 0. */
102
60
  export const CUSTOM_ROW_NUMBER = 0;
103
- /** The label a question earns for an answer the model did not list. */
104
- const CUSTOM_ROW_LABEL = 'other';
105
- /** What the free-text row does, said where the reader decides. */
106
- const CUSTOM_ROW_DESCRIPTION = 'type your own answer';
107
- /** How the list hint names that row, which the row itself already labels. */
108
- const CUSTOM_ROW_SHORTHAND = `${CUSTOM_ROW_NUMBER} answer freely`;
109
- /**
110
- * How a hint names the keys of one action.
111
- *
112
- * A hint is prose about what a key does here, so the word comes from the card
113
- * rather than from the catalog row, which has to read well out of context too.
114
- */
115
- function namedKeys(map, id, verb) {
116
- // One verb for the action, however many keys reach it: a hint that repeated
117
- // the word would read as two actions rather than two ways to do one.
118
- return `${keysFor(map, id).map(keyName).join('/')} ${verb}`;
119
- }
120
- /**
121
- * How the free-text row spells the keys that leave it.
122
- *
123
- * A text field keeps its own exits: the arrows and escape are the field's, not
124
- * the question's, so a reader who moved question.up or question.skip somewhere
125
- * else still leaves with these. The question's own movement keys leave it as
126
- * well, which is how the navigation aliases reach a reader who is typing rather
127
- * than filtering. The arrows stay one token because they are pinned together
128
- * rather than listed one direction at a time.
129
- */
130
- function customRowExits(map) {
131
- const names = ['↑↓'];
132
- for (const id of ['question.up', 'question.down']) {
133
- for (const key of keysFor(map, id)) {
134
- const name = keyName(key);
135
- if (name !== 'up' && name !== 'down' && !names.includes(name))
136
- names.push(name);
137
- }
138
- }
139
- return names.join('/');
140
- }
141
- /**
142
- * Whether a press leaves the free-text row for the option list.
143
- *
144
- * The arrows are the field's own, so they leave whatever the map says, which
145
- * keeps a reader who moved the question keys from being stranded in the field;
146
- * the question's own keys follow the map so a rebound alias still works here.
147
- */
148
- function leavesCustomRow(map, data) {
149
- return matchesKey(data, 'up') || matchesKey(data, 'down')
150
- || matchesAction(map, 'question.up', data) || matchesAction(map, 'question.down', data);
151
- }
152
- /** The keys that answer the free-text row, where typing is the answer rather than a filter. */
153
- function customHint(map) {
154
- return `type or paste an answer · ${namedKeys(map, 'question.confirm', 'confirm')} · ${namedKeys(map, 'question.cancel', 'abandon')} · ${customRowExits(map)} or esc to options`;
155
- }
156
- /**
157
- * The question-id suffix that declares a typed answer a credential. The seam
158
- * has no field for it, so a caller marks its own id and the gate reads that
159
- * declaration rather than the wording: ids are caller-owned, and a question
160
- * that merely mentions a key must still show the answer its author meant read.
161
- */
162
- const SECRET_ID_SUFFIX = ':secret';
163
- /** The free-text row's label when the answer is a credential, so the reader sees what they hand over. */
164
- const SECRET_ROW_LABEL = 'API KEY';
165
- /** Whether a question declares its typed answer a credential through its id. */
166
- function declaresSecret(id) {
167
- return id.endsWith(SECRET_ID_SUFFIX);
168
- }
169
- /**
170
- * A pending batch of questions.
171
- *
172
- * Answers are collected per question and returned in the seam's shape. An
173
- * escape skips the current question with an empty selection rather than
174
- * aborting the batch, so a human who cannot answer one question still returns
175
- * everything else they decided.
176
- */
177
- export class QuestionGate {
178
- questions;
179
- input;
180
- keys;
181
- index = 0;
182
- cursor = 0;
183
- typed = '';
184
- /** Whether the cursor is on the free-text row. */
185
- atCustom = false;
186
- /**
187
- * The row an escape from the free-text row returns to.
188
- *
189
- * It is the row the reader last acted on — walked to, picked by its number, or
190
- * toggled — because that is the option they were looking at when they decided
191
- * to answer freely, and it is not always the row the cursor still stands on: a
192
- * digit picks by number without walking anywhere.
193
- */
194
- customFrom = 0;
195
- chosen = [];
196
- custom = [];
197
- finished = false;
198
- constructor(questions,
199
- /** The editor every typed answer is written in, whichever row asks for it. */
200
- input,
201
- /** The keys in force, read per press so a settings edit lands on the next key. */
202
- keys) {
203
- this.questions = questions;
204
- this.input = input;
205
- this.keys = keys;
206
- for (const _ of questions) {
207
- this.chosen.push([]);
208
- this.custom.push(undefined);
209
- }
210
- this.resetInput();
211
- }
212
- /**
213
- * Empty the editor for the question now under the cursor.
214
- *
215
- * The editor belongs to the surface and outlives one question, so an answer
216
- * must never cross from the last question into the next — or from a gate that
217
- * was abandoned into the one that follows it. Its mode follows the question
218
- * for the same reason: a credential must not be shown because the last
219
- * question was not one.
220
- */
221
- resetInput() {
222
- this.input.setText('');
223
- this.input.setMode?.(this.current !== undefined && declaresSecret(this.current.id) ? 'secret' : 'answer');
224
- }
225
- get resolved() {
226
- return this.finished;
227
- }
228
- /**
229
- * Stop a question whose caller is gone.
230
- *
231
- * WHY: an aborted call has no reader for the answer, and a live gate would
232
- * otherwise keep the keyboard — and the borrowed editor — until someone
233
- * typed into a call that had already ended.
234
- */
235
- cancel() {
236
- this.finished = true;
237
- }
238
- get current() {
239
- return this.questions[this.index];
240
- }
241
- /**
242
- * Options the typed filter leaves, in the order they were listed.
243
- *
244
- * The order is not the score order a picker uses: a digit answers by
245
- * position, and re-ranking rows would move the number the reader just read.
246
- */
247
- matched(question) {
248
- const needle = this.typed.trim();
249
- return question.options
250
- .map((option, position) => ({ option, position }))
251
- .filter(({ option }) => matchScore(needle, `${option.label} ${option.description ?? ''}`) !== undefined);
252
- }
253
- /**
254
- * The rows the keys and the card agree on: the filter, then a window around
255
- * the cursor, because a list as long as a provider catalog would otherwise
256
- * push the editor off the screen.
257
- */
258
- windowed(question) {
259
- const matched = this.matched(question);
260
- const cursor = Math.min(this.cursor, Math.max(0, matched.length - 1));
261
- const start = Math.max(0, Math.min(cursor - Math.floor(QUESTION_WINDOW / 2), matched.length - QUESTION_WINDOW));
262
- return { rows: matched.slice(start, start + QUESTION_WINDOW), start, cursor };
263
- }
264
- /** The row under the cursor, as the filter leaves it. */
265
- currentRow(question) {
266
- const matched = this.matched(question);
267
- return matched[Math.min(this.cursor, Math.max(0, matched.length - 1))];
268
- }
269
- /** Take or drop one source option, by single-select replacement or multi-select toggle. */
270
- pick(position) {
271
- const question = this.current;
272
- const option = question?.options[position];
273
- if (question === undefined || option === undefined)
274
- return;
275
- const chosen = this.chosen[this.index] ?? [];
276
- this.chosen[this.index] = chosen.includes(option.label)
277
- ? chosen.filter(label => label !== option.label)
278
- : question.multiSelect === true ? [...chosen, option.label] : [option.label];
279
- }
280
- /**
281
- * Whether a keystroke is answer text rather than a filter.
282
- *
283
- * A question without options has no list to filter, and the free-text row is
284
- * where a question with options collects an answer the model did not list.
285
- */
286
- typingAnswer(question) {
287
- return question.options.length === 0 || this.atCustom;
288
- }
289
- /** Add text to the filter, or to the answer the current row collects. */
290
- absorb(text) {
291
- const question = this.current;
292
- if (question === undefined || text === '')
293
- return;
294
- if (this.typingAnswer(question)) {
295
- // The editor decides what an insertion is: a paste arrives as one edit,
296
- // and a multi-character chunk is one undo step rather than many.
297
- this.input.handleInput(text);
298
- return;
299
- }
300
- this.typed += text;
301
- this.cursor = 0;
302
- this.customFrom = 0;
303
- }
304
- /** Apply one key press; returns the batch answer the first time it completes. */
305
- handleKey(data) {
306
- const question = this.current;
307
- if (this.finished || question === undefined)
308
- return undefined;
309
- const paste = pastedText(data);
310
- if (paste !== undefined) {
311
- // An answer takes the paste sequence itself, because the editor reads it;
312
- // a filter takes the text inside it, because nothing else would.
313
- this.absorb(this.typingAnswer(question) ? data : paste);
314
- return undefined;
315
- }
316
- // Abandoning the batch is the one decision that belongs to no row: the
317
- // reader asked to be out of the questions, and the row the cursor happens
318
- // to sit on is not an answer to that. It settles the way an aborted call
319
- // does, with no answers at all, so a partial batch is never handed over as
320
- // if the reader had walked through it.
321
- if (matchesAction(this.keys(), 'question.cancel', data)) {
322
- this.cancel();
323
- return [];
324
- }
325
- // The free-text row is a text field, so the keys that walk or pick a list
326
- // would otherwise take the very characters an answer is made of.
327
- if (this.atCustom)
328
- return this.handleCustomKey(data);
329
- const matched = this.matched(question);
330
- const keys = this.keys();
331
- if (matchesAction(keys, 'question.up', data)) {
332
- this.cursor = Math.max(0, this.cursor - 1);
333
- this.customFrom = this.cursor;
334
- return undefined;
335
- }
336
- if (matchesAction(keys, 'question.down', data)) {
337
- // Below the last option sits the free-text row, which is the second way
338
- // to reach it; a list with nothing left to show steps straight onto it.
339
- if (this.cursor >= matched.length - 1) {
340
- this.atCustom = true;
341
- }
342
- else {
343
- this.cursor += 1;
344
- this.customFrom = this.cursor;
345
- }
346
- return undefined;
347
- }
348
- if (question.options.length > 0 && matchesAction(keys, 'question.toggle', data)) {
349
- const row = this.currentRow(question);
350
- if (row !== undefined)
351
- this.pick(row.position);
352
- this.customFrom = this.cursor;
353
- return undefined;
354
- }
355
- if (matchesAction(keys, 'question.skip', data)) {
356
- this.advance();
357
- return this.result();
358
- }
359
- if (matchesAction(keys, 'question.confirm', data)) {
360
- const chosen = this.chosen[this.index] ?? [];
361
- if (question.options.length > 0 && chosen.length === 0) {
362
- // A reader who typed enough to narrow the list is naming the row it
363
- // left under the cursor; one who typed an id the list does not hold is
364
- // naming that instead, which is why the typed text becomes the answer.
365
- const row = this.currentRow(question);
366
- if (row !== undefined)
367
- this.pick(row.position);
368
- else if (this.typed.trim() !== '')
369
- this.custom[this.index] = this.typed.trim();
370
- }
371
- this.confirm();
372
- return this.result();
373
- }
374
- if (matchesKey(data, 'backspace')) {
375
- // The same routing typing follows: a question without options has no
376
- // filter to erase, and the delete belongs to the answer it collected.
377
- if (this.typingAnswer(question)) {
378
- this.input.handleInput(data);
379
- return undefined;
380
- }
381
- this.typed = this.typed.slice(0, -1);
382
- this.cursor = 0;
383
- return undefined;
384
- }
385
- if (matchesKey(data, 'space')) {
386
- this.absorb(' ');
387
- return undefined;
388
- }
389
- if (question.options.length > 0 && /^[0-9]$/u.test(data)) {
390
- const digit = Number.parseInt(data, 10);
391
- if (digit === CUSTOM_ROW_NUMBER) {
392
- this.atCustom = true;
393
- return undefined;
394
- }
395
- const row = matched[digit - 1];
396
- if (row !== undefined) {
397
- this.pick(row.position);
398
- this.customFrom = digit - 1;
399
- }
400
- return undefined;
401
- }
402
- if (data.length === 1 && data >= ' ')
403
- this.absorb(data);
404
- return undefined;
405
- }
406
- /**
407
- * One key press while the free-text row holds the cursor.
408
- *
409
- * Walking back to the list keeps whatever was typed, so a reader who changes
410
- * their mind about answering freely has not lost the text yet. An escape
411
- * leaves the row the same way, because a question skipped by accident is a
412
- * question the reader has to answer again: from the list, where every other
413
- * question is skipped, an escape skips this one too.
414
- */
415
- handleCustomKey(data) {
416
- if (matchesAction(this.keys(), 'question.confirm', data)) {
417
- this.confirm();
418
- return this.result();
419
- }
420
- // The keys that leave a text field are the field's own, not the question's:
421
- // a skip the reader moved elsewhere must not quietly mean "walk back". The
422
- // rows the question does own still work here, and the hint names both
423
- // (customRowExits).
424
- if (leavesCustomRow(this.keys(), data)) {
425
- this.atCustom = false;
426
- return undefined;
427
- }
428
- if (matchesKey(data, 'escape')) {
429
- const question = this.current;
430
- const rows = question === undefined ? 0 : this.matched(question).length;
431
- this.atCustom = false;
432
- this.cursor = Math.min(this.customFrom, Math.max(0, rows - 1));
433
- return undefined;
434
- }
435
- // Anything else is the editor's: movement, deletion, undo, and the keys
436
- // that insert a character this gate has no business knowing about.
437
- this.input.handleInput(data);
438
- return undefined;
439
- }
440
- /** Take the answer the reader confirmed, then leave the question. */
441
- confirm() {
442
- const question = this.current;
443
- if (question === undefined)
444
- return;
445
- const text = this.input.getExpandedText().trim();
446
- if (text !== '') {
447
- this.custom[this.index] = text;
448
- // The seam reads a single-select custom as the answer itself, so a label
449
- // picked before it would be a second answer the caller has to reconcile.
450
- if (question.multiSelect !== true)
451
- this.chosen[this.index] = [];
452
- }
453
- this.advance();
454
- }
455
- /**
456
- * Leave the current question for the next one.
457
- *
458
- * Typed text belongs to the question being left, so it never crosses into the
459
- * next one — which is why a skipped question reports no typed answer at all.
460
- */
461
- advance() {
462
- this.typed = '';
463
- this.atCustom = false;
464
- this.customFrom = 0;
465
- this.cursor = 0;
466
- this.index += 1;
467
- if (this.index >= this.questions.length)
468
- this.finished = true;
469
- this.resetInput();
470
- }
471
- result() {
472
- if (!this.finished)
473
- return undefined;
474
- return this.questions.map((question, position) => {
475
- const custom = this.custom[position];
476
- return {
477
- id: question.id,
478
- selected: this.chosen[position] ?? [],
479
- ...(custom === undefined ? {} : { custom }),
480
- };
481
- });
482
- }
483
- card() {
484
- const question = this.current;
485
- if (question === undefined) {
486
- return { kind: 'question', title: 'question', detail: [], optionOffset: 0, options: [], custom: undefined, answerInput: undefined, hint: 'finishing' };
487
- }
488
- const chosen = this.chosen[this.index] ?? [];
489
- const detail = question.detail === undefined ? [] : lines(question.detail);
490
- const heading = question.header === undefined ? '' : question.header + ' · ';
491
- const title = `${heading}${question.question}${this.questions.length > 1 ? ` (${this.index + 1}/${this.questions.length})` : ''}`;
492
- if (question.options.length === 0) {
493
- return {
494
- kind: 'question',
495
- title,
496
- detail,
497
- optionOffset: 0,
498
- options: [],
499
- custom: undefined,
500
- // The editor is the only place the text lands, so it is drawn even while
501
- // empty: a question answered by typing needs somewhere to paste a key.
502
- answerInput: this.input,
503
- hint: `type or paste an answer · ${namedKeys(this.keys(), 'question.confirm', 'confirm')} · ${namedKeys(this.keys(), 'question.skip', 'skip')} · ${namedKeys(this.keys(), 'question.cancel', 'abandon')}`,
504
- };
505
- }
506
- if (this.typed !== '')
507
- detail.push(`filter: ${this.typed}`);
508
- // The editor holds the answer, so whether one has been written is its own
509
- // answer to give; the card only decides where to draw it.
510
- const written = this.input.getExpandedText() !== '';
511
- const secret = declaresSecret(question.id);
512
- const matched = this.matched(question);
513
- const { rows, start, cursor } = this.windowed(question);
514
- if (rows.length < matched.length)
515
- detail.push(`showing ${start + 1}–${start + rows.length} of ${matched.length}`);
516
- return {
517
- kind: 'question',
518
- title,
519
- detail,
520
- optionOffset: start,
521
- options: rows.map(({ option }, position) => ({
522
- label: option.label,
523
- description: option.description,
524
- // The free-text row wears the cursor when it holds it: a mark on a row
525
- // the reader is not pointing at is a mark that lies.
526
- current: !this.atCustom && start + position === cursor,
527
- selected: chosen.includes(option.label),
528
- })),
529
- custom: {
530
- label: secret ? SECRET_ROW_LABEL : CUSTOM_ROW_LABEL,
531
- description: CUSTOM_ROW_DESCRIPTION,
532
- current: this.atCustom,
533
- selected: this.atCustom || written,
534
- },
535
- // A written answer stays in view whether or not the cursor is on the row,
536
- // because it is what an enter is about to send.
537
- answerInput: this.atCustom || written ? this.input : undefined,
538
- hint: this.atCustom
539
- ? customHint(this.keys())
540
- : `${namedKeys(this.keys(), 'question.toggle', question.multiSelect ? 'toggle' : 'select')} · digits pick · ${CUSTOM_ROW_SHORTHAND} · type to filter · ${namedKeys(this.keys(), 'question.confirm', 'confirm')} · ${namedKeys(this.keys(), 'question.skip', 'skip')} · ${namedKeys(this.keys(), 'question.cancel', 'abandon')}`,
541
- };
542
- }
543
- }
544
61
  //# sourceMappingURL=gates.js.map