pi-cc-extensions 0.8.30 → 0.8.32

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 (53) hide show
  1. package/README.en.md +164 -174
  2. package/README.md +113 -115
  3. package/extensions/claude-code-style.ts +371 -208
  4. package/extensions/compact-thinking.ts +58 -0
  5. package/extensions/fixed-editor.ts +185 -0
  6. package/extensions/index.ts +10 -4
  7. package/extensions/subagent-notification.ts +30 -23
  8. package/node_modules/@tifan/pi-fixed-editor/README.md +36 -0
  9. package/node_modules/@tifan/pi-fixed-editor/package.json +46 -0
  10. package/node_modules/@tifan/pi-fixed-editor/src/cluster.ts +113 -0
  11. package/node_modules/@tifan/pi-fixed-editor/src/index.ts +259 -0
  12. package/node_modules/@tifan/pi-fixed-editor/src/terminal-split.ts +1337 -0
  13. package/package.json +9 -5
  14. package/extensions/ask-user-question/LICENSE +0 -21
  15. package/extensions/ask-user-question/README.md +0 -63
  16. package/extensions/ask-user-question/ask-user-question.ts +0 -266
  17. package/extensions/ask-user-question/config.ts +0 -79
  18. package/extensions/ask-user-question/events.ts +0 -55
  19. package/extensions/ask-user-question/index.ts +0 -17
  20. package/extensions/ask-user-question/reconcile.ts +0 -47
  21. package/extensions/ask-user-question/rpc-fallback.ts +0 -172
  22. package/extensions/ask-user-question/state/build-questionnaire.ts +0 -314
  23. package/extensions/ask-user-question/state/dialog-height.ts +0 -16
  24. package/extensions/ask-user-question/state/key-router.ts +0 -291
  25. package/extensions/ask-user-question/state/questionnaire-session.ts +0 -193
  26. package/extensions/ask-user-question/state/row-intent.ts +0 -143
  27. package/extensions/ask-user-question/state/selectors/contract.ts +0 -24
  28. package/extensions/ask-user-question/state/selectors/derivations.ts +0 -40
  29. package/extensions/ask-user-question/state/selectors/focus.ts +0 -17
  30. package/extensions/ask-user-question/state/selectors/projections.ts +0 -102
  31. package/extensions/ask-user-question/state/state-reducer.ts +0 -299
  32. package/extensions/ask-user-question/state/state.ts +0 -49
  33. package/extensions/ask-user-question/tool/format-answer.ts +0 -29
  34. package/extensions/ask-user-question/tool/response-envelope.ts +0 -50
  35. package/extensions/ask-user-question/tool/types.ts +0 -149
  36. package/extensions/ask-user-question/tool/validate-questionnaire.ts +0 -64
  37. package/extensions/ask-user-question/view/component-binding.ts +0 -45
  38. package/extensions/ask-user-question/view/components/inline-input.ts +0 -109
  39. package/extensions/ask-user-question/view/components/multi-select-view.ts +0 -198
  40. package/extensions/ask-user-question/view/components/option-list-view.ts +0 -77
  41. package/extensions/ask-user-question/view/components/preview/markdown-content-cache.ts +0 -76
  42. package/extensions/ask-user-question/view/components/preview/preview-block-renderer.ts +0 -114
  43. package/extensions/ask-user-question/view/components/preview/preview-box-renderer.ts +0 -86
  44. package/extensions/ask-user-question/view/components/preview/preview-layout-decider.ts +0 -219
  45. package/extensions/ask-user-question/view/components/preview/preview-pane.ts +0 -230
  46. package/extensions/ask-user-question/view/components/submit-picker.ts +0 -66
  47. package/extensions/ask-user-question/view/components/tab-bar.ts +0 -57
  48. package/extensions/ask-user-question/view/components/wrapping-select.ts +0 -307
  49. package/extensions/ask-user-question/view/dialog-builder.ts +0 -225
  50. package/extensions/ask-user-question/view/props-adapter.ts +0 -123
  51. package/extensions/ask-user-question/view/stateful-view.ts +0 -24
  52. package/extensions/ask-user-question/view/tab-components.ts +0 -16
  53. package/extensions/ask-user-question/view/tab-content-strategy.ts +0 -264
@@ -1,172 +0,0 @@
1
- /**
2
- * RPC / dialog-primitive fallback for `ask_user_question`.
3
- *
4
- * The canonical TUI path (`ctx.ui.custom()`) renders a tabbed temporary editor that
5
- * needs a real terminal. RPC-mode hosts (the VSCode pendant, ACP clients such
6
- * as Zed or Paseo) report `hasUI: true` because pi's dialog sub-protocol
7
- * (`extension_ui_request`/`extension_ui_response`) works, but `ui.custom()`
8
- * resolves `undefined` without rendering anything (issue #78). `ui.select()`
9
- * and `ui.input()` ARE functional there — the host renders them natively — so
10
- * this module walks the questions sequentially with those primitives and
11
- * returns the same `QuestionnaireResult` shapes the TUI produces, feeding the
12
- * shared `buildQuestionnaireResponse` envelope.
13
- *
14
- * Parity trade-offs vs the TUI, inherent to the select/input API surface: no
15
- * side-by-side preview pane (previews are folded into the prompt title), no
16
- * tabbed multi-question review (one dialog per question), and multi-select is
17
- * a free-text numbers input instead of checkbox rows. The "Type something."
18
- * escape is preserved on both variants — multi-select treats any non-index
19
- * input as a typed custom answer — matching
20
- * `ROW_INTENT_META.other.autoAppendOnMultiSelect`.
21
- */
22
-
23
- import { ROW_INTENT_META } from "./state/row-intent.js";
24
- import type {
25
- QuestionAnswer,
26
- QuestionData,
27
- QuestionnaireResult,
28
- QuestionParams,
29
- } from "./tool/types.js";
30
-
31
- const MULTI_SELECT_INSTRUCTIONS =
32
- 'Enter the numbers of all that apply, comma-separated (e.g. "1,3"), or type a custom answer as plain text.';
33
- const CUSTOM_ANSWER_TITLE = "Type your answer:";
34
- const MULTI_SELECT_PLACEHOLDER = "1,3";
35
-
36
- /** Longest preview slice folded into a select title before truncation. */
37
- const MAX_PREVIEW_CHARS = 600;
38
-
39
- /**
40
- * The dialog-primitive slice of `ExtensionUIContext` this walker needs.
41
- * Structural on purpose: the pinned pi 0.74 peer types predate `ctx.mode`,
42
- * and jiti transpiles without type-checking — `hasDialogUI` is the runtime
43
- * gate that makes the shape trustworthy.
44
- */
45
- export type DialogUI = {
46
- select: (title: string, options: string[]) => Promise<string | undefined>;
47
- input: (title: string, placeholder?: string) => Promise<string | undefined>;
48
- };
49
-
50
- /** True when the host implements the select/input dialog primitives. */
51
- export function hasDialogUI(ui: unknown): ui is DialogUI {
52
- const u = ui as Partial<Record<"select" | "input", unknown>> | null | undefined;
53
- return typeof u?.select === "function" && typeof u?.input === "function";
54
- }
55
-
56
- type Option = QuestionData["options"][number];
57
-
58
- function formatOptionLine(option: Option, index: number): string {
59
- return `${index + 1}. ${option.label} — ${option.description}`;
60
- }
61
-
62
- /**
63
- * Parse a user-entered (or select-returned) "N…" token to a 0-based option
64
- * index. `parseInt` reads the leading digits of "2. B — b" as 2; NaN and
65
- * out-of-range fail the bounds check and return null.
66
- */
67
- function parseIndex(token: string, count: number): number | null {
68
- const i = Number.parseInt(token, 10) - 1;
69
- return i >= 0 && i < count ? i : null;
70
- }
71
-
72
- /** Previews folded into the select title — RPC has no side-by-side pane. */
73
- function buildPreviewBlock(question: QuestionData): string {
74
- const blocks = question.options.flatMap((o, i) =>
75
- o.preview && o.preview.length > 0
76
- ? [`--- ${i + 1}. ${o.label} preview ---\n${o.preview.slice(0, MAX_PREVIEW_CHARS)}`]
77
- : [],
78
- );
79
- return blocks.length > 0 ? `\n\n${blocks.join("\n\n")}` : "";
80
- }
81
-
82
- /**
83
- * Walk the questionnaire one native dialog at a time. Dismissing any dialog
84
- * (the primitive resolves `undefined`) cancels the whole questionnaire —
85
- * mirroring Esc in the TUI — and the shared envelope emits DECLINE. A
86
- * `QuestionAnswer` is produced per question otherwise, so the envelope is
87
- * identical to the TUI path's.
88
- */
89
- export async function runRpcQuestionnaire(
90
- ui: DialogUI,
91
- params: QuestionParams,
92
- ): Promise<QuestionnaireResult> {
93
- const answers: QuestionAnswer[] = [];
94
- for (let qi = 0; qi < params.questions.length; qi++) {
95
- const q = params.questions[qi];
96
- const header = q.header ? `[${q.header}] ` : "";
97
- const answer = q.multiSelect
98
- ? await askMultiSelect(ui, q, qi, header)
99
- : await askSingleSelect(ui, q, qi, header);
100
- if (answer === undefined) return { answers, cancelled: true };
101
- answers.push(answer);
102
- }
103
- return { answers, cancelled: false };
104
- }
105
-
106
- /** `undefined` means the user dismissed the dialog (cancel the questionnaire). */
107
- async function askSingleSelect(
108
- ui: DialogUI,
109
- q: QuestionData,
110
- questionIndex: number,
111
- header: string,
112
- ): Promise<QuestionAnswer | undefined> {
113
- const options = q.options.map(formatOptionLine);
114
- options.push(`${q.options.length + 1}. ${ROW_INTENT_META.other.label}`);
115
- const chosen = await ui.select(`${header}${q.question}${buildPreviewBlock(q)}`, options);
116
- if (chosen == null) return undefined;
117
- const idx = parseIndex(chosen, options.length);
118
- // A host returning something outside the offered list is indistinguishable
119
- // from a dismissal — treat it as one rather than fabricate an answer.
120
- if (idx == null) return undefined;
121
- if (idx < q.options.length) {
122
- const o = q.options[idx];
123
- return {
124
- questionIndex,
125
- question: q.question,
126
- kind: "option",
127
- answer: o.label,
128
- preview: o.preview && o.preview.length > 0 ? o.preview : undefined,
129
- };
130
- }
131
- // "Type something." sentinel → free-text follow-up.
132
- const typed = await ui.input(`${header}${q.question}\n\n${CUSTOM_ANSWER_TITLE}`, "");
133
- if (typed == null) return undefined;
134
- return { questionIndex, question: q.question, kind: "custom", answer: typed };
135
- }
136
-
137
- /** `undefined` means the user dismissed the dialog (cancel the questionnaire). */
138
- async function askMultiSelect(
139
- ui: DialogUI,
140
- q: QuestionData,
141
- questionIndex: number,
142
- header: string,
143
- ): Promise<QuestionAnswer | undefined> {
144
- const list = q.options.map(formatOptionLine).join("\n");
145
- const value = await ui.input(
146
- `${header}${q.question}\n\n${list}\n\n${MULTI_SELECT_INSTRUCTIONS}`,
147
- MULTI_SELECT_PLACEHOLDER,
148
- );
149
- if (value == null) return undefined;
150
- const trimmed = value.trim();
151
- if (trimmed.length === 0) {
152
- // Deliberate empty commit — same as pressing "Next" with nothing toggled.
153
- return { questionIndex, question: q.question, kind: "multi", answer: null, selected: [] };
154
- }
155
- const tokens = trimmed.split(/[,\s]+/).filter((tok) => tok.length > 0);
156
- const indices = tokens.map((tok) =>
157
- /^\d+\.?$/.test(tok) ? parseIndex(tok, q.options.length) : null,
158
- );
159
- if (indices.every((i): i is number => i != null)) {
160
- const selected: string[] = [];
161
- for (const i of indices) {
162
- const label = q.options[i].label;
163
- if (!selected.includes(label)) selected.push(label);
164
- }
165
- return { questionIndex, question: q.question, kind: "multi", answer: null, selected };
166
- }
167
- // Any non-index token (words, or an out-of-range number like "13" for three
168
- // options) means the user typed an answer, not a selection. Preserve it
169
- // verbatim as a custom answer instead of silently dropping their input —
170
- // this is also the multi-select "Type something." escape.
171
- return { questionIndex, question: q.question, kind: "custom", answer: trimmed };
172
- }
@@ -1,314 +0,0 @@
1
- import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
2
- import { Input } from "@earendil-works/pi-tui";
3
- import type { QuestionData } from "../tool/types.js";
4
- import {
5
- type BoundGlobalBinding,
6
- type BoundPerTabBinding,
7
- globalBinding,
8
- perTabBinding,
9
- } from "../view/component-binding.js";
10
- import { MultiSelectView } from "../view/components/multi-select-view.js";
11
- import { OptionListView } from "../view/components/option-list-view.js";
12
- import { PreviewBlockRenderer } from "../view/components/preview/preview-block-renderer.js";
13
- import { crossTabLeftWidthWithDonation } from "../view/components/preview/preview-layout-decider.js";
14
- import type { PreviewPaneProps } from "../view/components/preview/preview-pane.js";
15
- import { PreviewPane } from "../view/components/preview/preview-pane.js";
16
- import { SubmitPicker } from "../view/components/submit-picker.js";
17
- import { TabBar } from "../view/components/tab-bar.js";
18
- import type {
19
- WrappingSelectItem,
20
- WrappingSelectTheme,
21
- } from "../view/components/wrapping-select.js";
22
- import { DialogView } from "../view/dialog-builder.js";
23
- import { QuestionnairePropsAdapter } from "../view/props-adapter.js";
24
- import type { StatefulView } from "../view/stateful-view.js";
25
- import type { TabBodyHeights, TabComponents } from "../view/tab-components.js";
26
- import type { PerTabSelector } from "./selectors/contract.js";
27
- import { selectActivePreviewPaneIndex } from "./selectors/derivations.js";
28
- import {
29
- selectDialogProps,
30
- selectMultiSelectProps,
31
- selectOptionListProps,
32
- selectPreviewPaneProps,
33
- selectSubmitPickerProps,
34
- selectTabBarProps,
35
- } from "./selectors/projections.js";
36
- import type { QuestionnaireState } from "./state.js";
37
- import { getDialogMaxRows } from "./dialog-height.js";
38
-
39
- export interface QuestionnaireBuildConfig {
40
- tui: { terminal: { columns: number; rows: number }; requestRender(): void };
41
- theme: Theme;
42
- questions: readonly QuestionData[];
43
- itemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;
44
- isMulti: boolean;
45
- initialState: QuestionnaireState;
46
- getCurrentTab: () => number;
47
- }
48
-
49
- export interface QuestionnaireBuilt {
50
- adapter: QuestionnairePropsAdapter;
51
- notesInput: Input;
52
- inlineInput: Input;
53
- render: (width: number) => string[];
54
- invalidate: () => void;
55
- }
56
-
57
- interface HeightComputers {
58
- current: (width: number) => number;
59
- }
60
-
61
- function previewBodyHeights(pane: PreviewPane): (width: number) => TabBodyHeights {
62
- return (width) => ({
63
- current: pane.naturalHeight(width),
64
- max: pane.maxNaturalHeight(width),
65
- });
66
- }
67
-
68
- function multiSelectBodyHeights(view: MultiSelectView): (width: number) => TabBodyHeights {
69
- return (width) => {
70
- const h = view.naturalHeight(width);
71
- return { current: h, max: h };
72
- };
73
- }
74
-
75
- const isActiveTab: PerTabSelector<boolean> = (s, ctx) =>
76
- ctx.i === selectActivePreviewPaneIndex(s.currentTab, ctx.totalQuestions);
77
-
78
- /**
79
- * Pure factory: assembles every TUI component, the props adapter, and a
80
- * lifecycle handle. Session-state dependencies arrive via `getCurrentTab` and
81
- * the `inputBuffer` cell. Initial paint is delegated to
82
- * `adapter.apply(initialState)` (called by the session at construction-end);
83
- * no selector is invoked here.
84
- */
85
- export function buildQuestionnaire(config: QuestionnaireBuildConfig): QuestionnaireBuilt {
86
- return new QuestionnaireBuilder(config).build();
87
- }
88
-
89
- /**
90
- * Programming-by-intention assembly: each private method names one
91
- * construction step from `build()`. Read top-down; the class is build-only and
92
- * discarded once the handle is returned.
93
- */
94
- class QuestionnaireBuilder {
95
- private readonly tui: QuestionnaireBuildConfig["tui"];
96
- private readonly theme: Theme;
97
- private readonly questions: readonly QuestionData[];
98
- private readonly itemsByTab: ReadonlyArray<readonly WrappingSelectItem[]>;
99
- private readonly isMulti: boolean;
100
- private readonly initialState: QuestionnaireState;
101
- private readonly getCurrentTab: () => number;
102
-
103
- private readonly selectTheme: WrappingSelectTheme;
104
- private readonly markdownTheme = getMarkdownTheme();
105
- private readonly notesInput = new Input();
106
- private readonly inlineInput = new Input();
107
- private readonly getTerminalWidth = () => this.tui.terminal.columns;
108
- private readonly getTerminalRows = () => getDialogMaxRows(this.tui.terminal.rows);
109
-
110
- constructor(config: QuestionnaireBuildConfig) {
111
- this.tui = config.tui;
112
- this.theme = config.theme;
113
- this.questions = config.questions;
114
- this.itemsByTab = config.itemsByTab;
115
- this.isMulti = config.isMulti;
116
- this.initialState = config.initialState;
117
- this.getCurrentTab = config.getCurrentTab;
118
-
119
- this.selectTheme = this.makeSelectTheme();
120
- }
121
-
122
- build(): QuestionnaireBuilt {
123
- const tabs = this.buildTabComponents();
124
- this.injectGlobalLeftWidth(tabs);
125
- const submitPicker = this.buildSubmitPicker();
126
- const tabBar = this.buildTabBar();
127
- const heights = this.buildHeightComputers(tabs);
128
- const dialog = this.buildDialog(tabs, submitPicker, tabBar, heights);
129
- const globalBindings = this.buildGlobalBindings(dialog, submitPicker, tabBar);
130
- const perTabBindings = this.buildPerTabBindings();
131
- const adapter = this.buildAdapter(tabs, globalBindings, perTabBindings);
132
- return this.handle(adapter, dialog);
133
- }
134
-
135
- private makeSelectTheme(): WrappingSelectTheme {
136
- const t = this.theme;
137
- return {
138
- selectedText: (s) => t.fg("accent", t.bold(s)),
139
- description: (s) => t.fg("muted", s),
140
- scrollInfo: (s) => t.fg("dim", s),
141
- };
142
- }
143
-
144
- private buildTabComponents(): ReadonlyArray<TabComponents> {
145
- return this.questions.map((q, i) => this.buildTabFor(q, i));
146
- }
147
-
148
- private buildTabFor(question: QuestionData, index: number): TabComponents {
149
- const optionList = new OptionListView({
150
- items: this.itemsByTab[index] ?? [],
151
- theme: this.selectTheme,
152
- });
153
- const previewBlock = new PreviewBlockRenderer({
154
- question,
155
- theme: this.theme,
156
- markdownTheme: this.markdownTheme,
157
- });
158
- const preview = new PreviewPane({
159
- question,
160
- getTerminalWidth: this.getTerminalWidth,
161
- optionListView: optionList,
162
- previewBlock,
163
- });
164
- const multiSelect = question.multiSelect
165
- ? new MultiSelectView(this.theme, question)
166
- : undefined;
167
- const bodyHeights = this.buildBodyHeights(question, preview, multiSelect);
168
- return { optionList, preview, multiSelect, bodyHeights };
169
- }
170
-
171
- private buildBodyHeights(
172
- question: QuestionData,
173
- preview: PreviewPane,
174
- multiSelect: MultiSelectView | undefined,
175
- ): (width: number) => TabBodyHeights {
176
- return question.multiSelect
177
- ? multiSelectBodyHeights(multiSelect!)
178
- : previewBodyHeights(preview);
179
- }
180
-
181
- /**
182
- * Compute cross-tab max adaptive left width and inject into each PreviewPane.
183
- * Iterates all tabs and takes the maximum adaptive width.
184
- * Called after buildTabComponents, before buildDialog (so setGlobalLeftWidth
185
- * is set before any rendering occurs).
186
- */
187
- private injectGlobalLeftWidth(tabs: ReadonlyArray<TabComponents>): void {
188
- const questions = this.questions;
189
- const itemsByTab = this.itemsByTab;
190
- const tabsDescriptor = questions.map((q) => ({ multiSelect: q.multiSelect }));
191
- // Memoize by paneWidth — preview/naturalHeight call this many times per keystroke.
192
- let cachedPaneWidth: number | undefined;
193
- let cachedLeftWidth = 0;
194
- const globalLeftWidth = (paneWidth: number): number => {
195
- if (cachedPaneWidth === paneWidth) return cachedLeftWidth;
196
- cachedLeftWidth = crossTabLeftWidthWithDonation(
197
- tabsDescriptor,
198
- itemsByTab,
199
- questions,
200
- paneWidth,
201
- );
202
- cachedPaneWidth = paneWidth;
203
- return cachedLeftWidth;
204
- };
205
- for (const tab of tabs) {
206
- tab.preview.setGlobalLeftWidth(globalLeftWidth);
207
- }
208
- }
209
-
210
- private buildSubmitPicker(): SubmitPicker | undefined {
211
- return this.isMulti ? new SubmitPicker(this.theme) : undefined;
212
- }
213
-
214
- private buildTabBar(): TabBar | undefined {
215
- return this.isMulti ? new TabBar(this.theme) : undefined;
216
- }
217
-
218
- private buildHeightComputers(tabs: ReadonlyArray<TabComponents>): HeightComputers {
219
- const current = (width: number): number => {
220
- const idx = Math.min(this.getCurrentTab(), tabs.length - 1);
221
- return Math.max(0, tabs[idx]?.bodyHeights(width).current ?? 0);
222
- };
223
- return { current };
224
- }
225
-
226
- private pickInitialActivePreview(
227
- tabs: ReadonlyArray<TabComponents>,
228
- ): StatefulView<PreviewPaneProps> {
229
- const idx = selectActivePreviewPaneIndex(this.initialState.currentTab, this.questions.length);
230
- return tabs[idx]?.preview ?? tabs[0]!.preview;
231
- }
232
-
233
- private buildDialog(
234
- tabs: ReadonlyArray<TabComponents>,
235
- submitPicker: SubmitPicker | undefined,
236
- tabBar: TabBar | undefined,
237
- heights: HeightComputers,
238
- ): DialogView {
239
- return new DialogView(
240
- {
241
- theme: this.theme,
242
- questions: this.questions,
243
- tabBar,
244
- notesInput: this.notesInput,
245
- isMulti: this.isMulti,
246
- tabsByIndex: tabs,
247
- submitPicker,
248
- getCurrentBodyHeight: heights.current,
249
- getTerminalRows: this.getTerminalRows,
250
- },
251
- { state: this.initialState, activePreviewPane: this.pickInitialActivePreview(tabs) },
252
- );
253
- }
254
-
255
- private buildGlobalBindings(
256
- dialog: DialogView,
257
- submitPicker: SubmitPicker | undefined,
258
- tabBar: TabBar | undefined,
259
- ): ReadonlyArray<BoundGlobalBinding> {
260
- return [
261
- globalBinding({ component: dialog, select: selectDialogProps }),
262
- ...(submitPicker
263
- ? [globalBinding({ component: submitPicker, select: selectSubmitPickerProps })]
264
- : []),
265
- ...(tabBar ? [globalBinding({ component: tabBar, select: selectTabBarProps })] : []),
266
- ];
267
- }
268
-
269
- private buildPerTabBindings(): ReadonlyArray<BoundPerTabBinding> {
270
- return [
271
- perTabBinding({
272
- resolve: (tab) => tab.optionList,
273
- predicate: isActiveTab,
274
- select: selectOptionListProps,
275
- }),
276
- perTabBinding({
277
- resolve: (tab) => tab.preview,
278
- predicate: isActiveTab,
279
- select: selectPreviewPaneProps,
280
- }),
281
- perTabBinding({
282
- resolve: (tab) => tab.multiSelect,
283
- select: selectMultiSelectProps,
284
- }),
285
- ];
286
- }
287
-
288
- private buildAdapter(
289
- tabs: ReadonlyArray<TabComponents>,
290
- globalBindings: ReadonlyArray<BoundGlobalBinding>,
291
- perTabBindings: ReadonlyArray<BoundPerTabBinding>,
292
- ): QuestionnairePropsAdapter {
293
- return new QuestionnairePropsAdapter({
294
- tui: this.tui,
295
- questions: this.questions,
296
- itemsByTab: this.itemsByTab,
297
- tabsByIndex: tabs,
298
- inlineInput: this.inlineInput,
299
- globalBindings,
300
- perTabBindings,
301
- extraInvalidatables: [this.notesInput],
302
- });
303
- }
304
-
305
- private handle(adapter: QuestionnairePropsAdapter, dialog: DialogView): QuestionnaireBuilt {
306
- return {
307
- adapter,
308
- notesInput: this.notesInput,
309
- inlineInput: this.inlineInput,
310
- render: (w) => dialog.render(w),
311
- invalidate: () => adapter.invalidate(),
312
- };
313
- }
314
- }
@@ -1,16 +0,0 @@
1
- const FULL_HEIGHT_BREAKPOINT = 16;
2
- const MIN_DIALOG_ROWS = 10;
3
- const MIN_HISTORY_ROWS = 6;
4
- const HISTORY_RATIO = 0.3;
5
-
6
- /**
7
- * Keep recent transcript rows visible above the temporary questionnaire editor.
8
- * Very small terminals fall back to full height so the dialog remains usable.
9
- */
10
- export function getDialogMaxRows(terminalRows: number): number {
11
- const rows = Math.max(1, Math.floor(terminalRows));
12
- if (rows < FULL_HEIGHT_BREAKPOINT) return rows;
13
-
14
- const historyRows = Math.max(MIN_HISTORY_ROWS, Math.floor(rows * HISTORY_RATIO));
15
- return Math.min(rows, Math.max(MIN_DIALOG_ROWS, rows - historyRows));
16
- }