@zergai/ztc 0.2.6-dev.36101352252 → 0.2.7-dev.36236183737
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.
- package/README.md +43 -17
- package/bin/zmux-darwin-arm64 +0 -0
- package/bin/ztc-tui-darwin-arm64 +0 -0
- package/bin/zterm-darwin-arm64 +0 -0
- package/dist/App.js +19 -5
- package/dist/agent/agent.d.ts +5 -1
- package/dist/agent/agent.js +51 -8
- package/dist/agent/backends/account_pool.d.ts +5 -6
- package/dist/agent/backends/account_pool.js +39 -14
- package/dist/agent/backends/account_rotation.d.ts +3 -0
- package/dist/agent/backends/account_rotation.js +14 -3
- package/dist/agent/backends/codex_account_status.d.ts +42 -0
- package/dist/agent/backends/codex_account_status.js +196 -0
- package/dist/agent/backends/codex_app_server.d.ts +12 -2
- package/dist/agent/backends/codex_app_server.js +66 -6
- package/dist/agent/backends/index.d.ts +2 -0
- package/dist/agent/backends/index.js +13 -5
- package/dist/agent/backends/openai.d.ts +2 -2
- package/dist/agent/backends/openai.js +4 -3
- package/dist/agent/backends/openai_compatible.js +10 -10
- package/dist/agent/backends/openai_responses.d.ts +17 -5
- package/dist/agent/backends/openai_responses.js +2 -4
- package/dist/agent/backends/token_usage.js +1 -1
- package/dist/agent/backends/types.d.ts +3 -0
- package/dist/agent/commands/accounts.d.ts +7 -1
- package/dist/agent/commands/accounts.js +299 -101
- package/dist/agent/commands/config.js +11 -3
- package/dist/agent/commands/config_profile_switch.d.ts +2 -0
- package/dist/agent/commands/config_profile_switch.js +22 -0
- package/dist/agent/commands/context.js +4 -3
- package/dist/agent/commands/index.js +1 -2
- package/dist/agent/commands/input_mode.js +1 -1
- package/dist/agent/commands/model.js +7 -2
- package/dist/agent/commands/plan.d.ts +12 -0
- package/dist/agent/commands/plan.js +39 -28
- package/dist/agent/commands/profile.d.ts +1 -1
- package/dist/agent/commands/profile.js +34 -257
- package/dist/agent/commands/provider_choices.d.ts +12 -1
- package/dist/agent/commands/provider_choices.js +29 -2
- package/dist/agent/commands/provider_selection.d.ts +3 -0
- package/dist/agent/commands/provider_selection.js +63 -0
- package/dist/agent/commands/settings.d.ts +2 -0
- package/dist/agent/commands/settings.js +655 -50
- package/dist/agent/commands/types.d.ts +19 -2
- package/dist/agent/tools/ask_user.js +2 -2
- package/dist/app/agent_runner.d.ts +1 -1
- package/dist/app/agent_runner.js +2 -2
- package/dist/app/ask_user_response.d.ts +3 -0
- package/dist/app/ask_user_response.js +16 -0
- package/dist/app/command_context.js +29 -13
- package/dist/app/controller.d.ts +17 -2
- package/dist/app/controller.js +24 -1
- package/dist/app/selection_completion.d.ts +9 -3
- package/dist/app/selection_completion.js +54 -22
- package/dist/cli_args.js +10 -1
- package/dist/cli_main.js +85 -2
- package/dist/cli_ratatui.d.ts +22 -4
- package/dist/cli_ratatui.js +421 -146
- package/dist/config/defaults.d.ts +6 -0
- package/dist/config/defaults.js +20 -0
- package/dist/config/profiles.d.ts +9 -0
- package/dist/config/profiles.js +87 -0
- package/dist/config/types.d.ts +2 -0
- package/dist/config.d.ts +7 -0
- package/dist/config.js +283 -40
- package/dist/headless.js +14 -0
- package/dist/panes/model_catalog.d.ts +1 -1
- package/dist/panes/model_catalog.js +24 -2
- package/dist/panes/ztc_launch.js +6 -0
- package/dist/router_auth.js +3 -2
- package/dist/session.d.ts +2 -0
- package/dist/session.js +3 -0
- package/dist/types.d.ts +23 -2
- package/dist/ui/views/app.d.ts +3 -1
- package/dist/ui/views/app.js +3 -2
- package/dist/ui/views/completion_menu.js +6 -6
- package/dist/ui/views/input_area.d.ts +2 -1
- package/dist/ui/views/input_area.js +9 -8
- package/dist/ui/views/status_bar.d.ts +2 -0
- package/dist/ui/views/status_bar.js +15 -4
- package/dist/ui/widgets/color_panel.d.ts +9 -0
- package/dist/ui/widgets/color_panel.js +41 -26
- package/dist/ui/widgets/navigator_panel.d.ts +9 -0
- package/dist/ui/widgets/navigator_panel.js +17 -16
- package/dist/ui/widgets/panel_frame.d.ts +83 -3
- package/dist/ui/widgets/panel_frame.js +107 -9
- package/dist/ui/widgets/questionnaire.d.ts +9 -0
- package/dist/ui/widgets/questionnaire.js +214 -16
- package/dist/ui/widgets/resume_panel.d.ts +9 -0
- package/dist/ui/widgets/resume_panel.js +18 -17
- package/dist/ui/widgets/rewind_panel.d.ts +9 -0
- package/dist/ui/widgets/rewind_panel.js +15 -16
- package/dist/ui/widgets/select.d.ts +6 -0
- package/dist/ui/widgets/select.js +47 -12
- package/dist/ui/widgets/settings_panel.d.ts +75 -0
- package/dist/ui/widgets/settings_panel.js +197 -39
- package/dist/ui/widgets/status_line_panel.d.ts +9 -0
- package/dist/ui/widgets/status_line_panel.js +18 -19
- package/dist/ui/widgets/transcript_preview.d.ts +8 -0
- package/dist/ui/widgets/transcript_preview.js +10 -0
- package/dist/utils/context.js +2 -0
- package/dist/utils/fireworks.d.ts +20 -0
- package/dist/utils/fireworks.js +23 -0
- package/dist/utils/models.d.ts +2 -1
- package/dist/utils/models.js +19 -0
- package/dist/web/accounts_mirror.d.ts +7 -0
- package/dist/web/accounts_mirror.js +59 -7
- package/dist/web/mirror_stream.js +19 -5
- package/dist/zlm-embed.mjs +5734 -5362
- package/package.json +2 -2
- package/dist/agent/commands/provider.d.ts +0 -2
- package/dist/agent/commands/provider.js +0 -75
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { displayWidth, sanitizeTerminalText } from '../core/text_width.js';
|
|
7
7
|
import { isControlKey } from '../core/input.js';
|
|
8
|
+
import { backspace, createEmptyState, deleteForward, deleteWordBackward, deleteWordForward, getPlainText, insertText, killToLineEnd, killToLineStart, moveLeft, moveRight, moveToEnd, moveToLineEnd, moveToLineStart, moveToStart, moveVertical, moveWordLeft, moveWordRight, } from '../core/input_segments.js';
|
|
8
9
|
import { box, text } from '../core/index.js';
|
|
9
10
|
import { TERMINAL_THEME } from '../views/theme.js';
|
|
11
|
+
import { renderLineWithCursorTokens, wrapInputSegments } from '../views/input_area.js';
|
|
10
12
|
import { bottomBorder, framedLine, lineText, topBorder, } from './panel_frame.js';
|
|
11
13
|
const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
12
14
|
const RAW_DELETE_INPUT = new Set(['\x7f', '\b', '\x08']);
|
|
@@ -97,6 +99,8 @@ export class QuestionnaireWidget {
|
|
|
97
99
|
otherMode = false;
|
|
98
100
|
otherValue = '';
|
|
99
101
|
textValues = {};
|
|
102
|
+
optionNotes = new Map();
|
|
103
|
+
editingOptionNote = false;
|
|
100
104
|
constructor(host) {
|
|
101
105
|
this.host = host;
|
|
102
106
|
}
|
|
@@ -116,6 +120,8 @@ export class QuestionnaireWidget {
|
|
|
116
120
|
this.otherMode = false;
|
|
117
121
|
this.otherValue = '';
|
|
118
122
|
this.textValues = {};
|
|
123
|
+
this.optionNotes.clear();
|
|
124
|
+
this.editingOptionNote = false;
|
|
119
125
|
// Pre-fill defaults
|
|
120
126
|
for (let i = 0; i < data.questions.length; i++) {
|
|
121
127
|
const q = data.questions[i];
|
|
@@ -146,6 +152,8 @@ export class QuestionnaireWidget {
|
|
|
146
152
|
this.otherMode = false;
|
|
147
153
|
this.otherValue = '';
|
|
148
154
|
this.textValues = {};
|
|
155
|
+
this.optionNotes.clear();
|
|
156
|
+
this.editingOptionNote = false;
|
|
149
157
|
}
|
|
150
158
|
isActive() {
|
|
151
159
|
return this.active !== null;
|
|
@@ -160,7 +168,8 @@ export class QuestionnaireWidget {
|
|
|
160
168
|
const answeredIndex = question.options.findIndex((option) => option.value === answer);
|
|
161
169
|
this.optionIndex = answeredIndex >= 0
|
|
162
170
|
? answeredIndex
|
|
163
|
-
: (this.active.allowOther
|
|
171
|
+
: (this.active.allowOther && !(this.active.allowOptionNotes && question.type === 'select')
|
|
172
|
+
? question.options.length : 0);
|
|
164
173
|
}
|
|
165
174
|
else {
|
|
166
175
|
// Unanswered question: land on the recommended option when the agent
|
|
@@ -169,6 +178,34 @@ export class QuestionnaireWidget {
|
|
|
169
178
|
}
|
|
170
179
|
this.otherMode = false;
|
|
171
180
|
this.otherValue = '';
|
|
181
|
+
this.editingOptionNote = false;
|
|
182
|
+
}
|
|
183
|
+
getOptionNote() {
|
|
184
|
+
return this.optionNotes.get(this.tabIndex) ?? createEmptyState();
|
|
185
|
+
}
|
|
186
|
+
setOptionNote(state) {
|
|
187
|
+
this.optionNotes.set(this.tabIndex, state);
|
|
188
|
+
this.host.render();
|
|
189
|
+
}
|
|
190
|
+
selectHighlightedOption(question) {
|
|
191
|
+
const option = question.options?.[this.optionIndex];
|
|
192
|
+
if (option)
|
|
193
|
+
this.answers[question.name] = option.value;
|
|
194
|
+
}
|
|
195
|
+
saveOptionNotes() {
|
|
196
|
+
if (!this.active?.allowOptionNotes)
|
|
197
|
+
return;
|
|
198
|
+
this.active.questions.forEach((question, index) => {
|
|
199
|
+
if (question.type !== 'select')
|
|
200
|
+
return;
|
|
201
|
+
const note = getPlainText((this.optionNotes.get(index) ?? createEmptyState()).segments);
|
|
202
|
+
// A note cleared after a failed submit must not resurface from the
|
|
203
|
+
// earlier attempt.
|
|
204
|
+
if (note.trim())
|
|
205
|
+
this.answers[`${question.name}_note`] = note;
|
|
206
|
+
else
|
|
207
|
+
delete this.answers[`${question.name}_note`];
|
|
208
|
+
});
|
|
172
209
|
}
|
|
173
210
|
submitAnswers() {
|
|
174
211
|
if (!this.active)
|
|
@@ -184,6 +221,7 @@ export class QuestionnaireWidget {
|
|
|
184
221
|
else
|
|
185
222
|
delete this.answers[question.name];
|
|
186
223
|
}
|
|
224
|
+
this.saveOptionNotes();
|
|
187
225
|
const firstMissing = questions.findIndex(question => question.required === true && !this.answers[question.name]);
|
|
188
226
|
if (firstMissing >= 0) {
|
|
189
227
|
this.host.setToast(`"${questions[firstMissing].question}" is required`, 3000);
|
|
@@ -242,12 +280,101 @@ export class QuestionnaireWidget {
|
|
|
242
280
|
}
|
|
243
281
|
return null;
|
|
244
282
|
}
|
|
283
|
+
handleOptionNoteInput(input, key, question) {
|
|
284
|
+
if (!this.active?.allowOptionNotes || question.type !== 'select')
|
|
285
|
+
return false;
|
|
286
|
+
if (isControlKey(input, key, 'c')) {
|
|
287
|
+
const callback = this.active.callback;
|
|
288
|
+
this.cancel();
|
|
289
|
+
this.host.render();
|
|
290
|
+
void Promise.resolve(callback(null)).catch(() => { });
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
if (!this.editingOptionNote) {
|
|
294
|
+
// A printable key starts the note for the highlighted option. Letter
|
|
295
|
+
// shortcuts must not steal the first character of the user's prose.
|
|
296
|
+
if (input && !key.ctrl && !key.meta && !key.escape && !key.return && !key.tab
|
|
297
|
+
&& !key.backspace && !key.delete && !key.upArrow && !key.downArrow
|
|
298
|
+
&& !key.leftArrow && !key.rightArrow && !key.home && !key.end
|
|
299
|
+
&& !/^[\x00-\x1f\x7f]$/.test(input)) {
|
|
300
|
+
this.selectHighlightedOption(question);
|
|
301
|
+
this.editingOptionNote = true;
|
|
302
|
+
this.setOptionNote(insertText(this.getOptionNote(), input.replace(/\r\n?/g, '\n')));
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
if (key.escape) {
|
|
308
|
+
this.editingOptionNote = false;
|
|
309
|
+
this.host.render();
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
if (key.return) {
|
|
313
|
+
if (key.shift) {
|
|
314
|
+
this.setOptionNote(insertText(this.getOptionNote(), '\n'));
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
this.selectHighlightedOption(question);
|
|
318
|
+
this.editingOptionNote = false;
|
|
319
|
+
this.advanceOrSubmit();
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (key.tab) {
|
|
324
|
+
this.editingOptionNote = false;
|
|
325
|
+
this.goToQuestion((this.tabIndex + 1) % this.active.questions.length);
|
|
326
|
+
this.host.render();
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
329
|
+
const state = this.getOptionNote();
|
|
330
|
+
if ((key.meta && RAW_DELETE_INPUT.has(input)) || isControlKey(input, key, 'w')
|
|
331
|
+
|| (key.backspace && (key.meta || key.ctrl))) {
|
|
332
|
+
this.setOptionNote(deleteWordBackward(state));
|
|
333
|
+
}
|
|
334
|
+
else if (key.backspace) {
|
|
335
|
+
this.setOptionNote(backspace(state));
|
|
336
|
+
}
|
|
337
|
+
else if (key.delete && (key.meta || key.ctrl)) {
|
|
338
|
+
this.setOptionNote(deleteWordForward(state));
|
|
339
|
+
}
|
|
340
|
+
else if (key.delete) {
|
|
341
|
+
this.setOptionNote(deleteForward(state));
|
|
342
|
+
}
|
|
343
|
+
else if (key.leftArrow) {
|
|
344
|
+
this.setOptionNote(key.meta || key.ctrl ? moveWordLeft(state) : moveLeft(state));
|
|
345
|
+
}
|
|
346
|
+
else if (key.rightArrow) {
|
|
347
|
+
this.setOptionNote(key.meta || key.ctrl ? moveWordRight(state) : moveRight(state));
|
|
348
|
+
}
|
|
349
|
+
else if (key.upArrow || key.downArrow) {
|
|
350
|
+
this.setOptionNote(moveVertical(state, key.upArrow ? -1 : 1).state);
|
|
351
|
+
}
|
|
352
|
+
else if (key.home || isControlKey(input, key, 'a')) {
|
|
353
|
+
this.setOptionNote(key.meta ? moveToStart(state) : moveToLineStart(state));
|
|
354
|
+
}
|
|
355
|
+
else if (key.end || isControlKey(input, key, 'e')) {
|
|
356
|
+
this.setOptionNote(key.meta ? moveToEnd(state) : moveToLineEnd(state));
|
|
357
|
+
}
|
|
358
|
+
else if (isControlKey(input, key, 'u')) {
|
|
359
|
+
this.setOptionNote(killToLineStart(state).state);
|
|
360
|
+
}
|
|
361
|
+
else if (isControlKey(input, key, 'k')) {
|
|
362
|
+
this.setOptionNote(killToLineEnd(state).state);
|
|
363
|
+
}
|
|
364
|
+
else if (input && !key.ctrl && !key.meta && !/^[\x00-\x1f\x7f]$/.test(input)) {
|
|
365
|
+
// Multi-character paste is one insertion, including its newlines.
|
|
366
|
+
this.setOptionNote(insertText(state, input.replace(/\r\n?/g, '\n')));
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
245
370
|
handleInput(input, key) {
|
|
246
371
|
if (!this.active)
|
|
247
372
|
return;
|
|
248
373
|
const questions = this.active.questions;
|
|
249
374
|
const totalTabs = questions.length;
|
|
250
375
|
const currentQ = questions[this.tabIndex];
|
|
376
|
+
if (currentQ && this.handleOptionNoteInput(input, key, currentQ))
|
|
377
|
+
return;
|
|
251
378
|
// If in "Other" text input mode for select/multiselect
|
|
252
379
|
if (this.otherMode && currentQ) {
|
|
253
380
|
if (key.escape) {
|
|
@@ -407,7 +534,8 @@ export class QuestionnaireWidget {
|
|
|
407
534
|
// Navigate options within current question tab: Up/Down
|
|
408
535
|
if (currentQ && (currentQ.type === 'select' || currentQ.type === 'multiselect')) {
|
|
409
536
|
const opts = currentQ.options || [];
|
|
410
|
-
const totalOpts = opts.length + (this.active.allowOther
|
|
537
|
+
const totalOpts = opts.length + (this.active.allowOther
|
|
538
|
+
&& !(this.active.allowOptionNotes && currentQ.type === 'select') ? 1 : 0);
|
|
411
539
|
if (key.upArrow || input === 'k') {
|
|
412
540
|
this.optionIndex = Math.max(0, this.optionIndex - 1);
|
|
413
541
|
this.host.render();
|
|
@@ -441,7 +569,8 @@ export class QuestionnaireWidget {
|
|
|
441
569
|
if (key.return) {
|
|
442
570
|
const idx = this.optionIndex;
|
|
443
571
|
// "Other" option
|
|
444
|
-
if (this.active.allowOther &&
|
|
572
|
+
if (this.active.allowOther && !(this.active.allowOptionNotes && currentQ.type === 'select')
|
|
573
|
+
&& idx === opts.length) {
|
|
445
574
|
this.otherMode = true;
|
|
446
575
|
this.otherValue = '';
|
|
447
576
|
this.host.render();
|
|
@@ -465,7 +594,7 @@ export class QuestionnaireWidget {
|
|
|
465
594
|
// printable character auto-jumps to Other with that character
|
|
466
595
|
// already captured in `otherValue`. See the analogous block in
|
|
467
596
|
// select.ts for the detailed rationale.
|
|
468
|
-
if (this.active.allowOther &&
|
|
597
|
+
if (this.active.allowOther && !this.active.allowOptionNotes &&
|
|
469
598
|
currentQ.type === 'select' &&
|
|
470
599
|
!this.otherMode &&
|
|
471
600
|
input &&
|
|
@@ -502,7 +631,7 @@ export class QuestionnaireWidget {
|
|
|
502
631
|
buildSurface(cols, maxRows) {
|
|
503
632
|
if (!this.active)
|
|
504
633
|
return { content: '', view: box([], {}), lineCount: 0 };
|
|
505
|
-
const { title, questions, allowOther } = this.active;
|
|
634
|
+
const { title, questions, allowOther, allowOptionNotes } = this.active;
|
|
506
635
|
const question = questions[this.tabIndex];
|
|
507
636
|
if (!question)
|
|
508
637
|
return { content: '', view: box([], {}), lineCount: 0 };
|
|
@@ -541,9 +670,29 @@ export class QuestionnaireWidget {
|
|
|
541
670
|
progress.push({ text: ` · ${question.header}`, style: { color: TERMINAL_THEME.subtle } });
|
|
542
671
|
}
|
|
543
672
|
lines.push(framed(progress));
|
|
544
|
-
|
|
673
|
+
// One content column for the whole card. The question, every option label,
|
|
674
|
+
// and every description start at this same column; the selection caret,
|
|
675
|
+
// number, and state marker are outdented into the gutter to its left. Lining
|
|
676
|
+
// them up separately made the card read as ragged (labels at 7, descriptions
|
|
677
|
+
// at 3, the question at 0).
|
|
678
|
+
// Size the outgoing gutter to the widest option prefix this question renders
|
|
679
|
+
// (`› 12. ○ ` is one cell wider than `› 1. ○ `), so the question text and
|
|
680
|
+
// every option label land on the same column.
|
|
681
|
+
// Mirror the exact option gutter, including its marker width: a multiselect
|
|
682
|
+
// checkbox (`[✓]`) is two cells wider than the select radio (`○`), so a
|
|
683
|
+
// hardcoded radio here put the question one column off its own labels.
|
|
684
|
+
const optionCount = question.options?.length ?? 1;
|
|
685
|
+
const widestIndex = allowOther ? optionCount + 1 : optionCount;
|
|
686
|
+
const numberWidth = String(Math.max(1, widestIndex)).length;
|
|
687
|
+
const markerWidth = question.type === 'multiselect' ? displayWidth('[✓]') : displayWidth('○');
|
|
688
|
+
// caret + space + (padded number) + space + marker + space
|
|
689
|
+
const gutter = ' '.repeat(displayWidth('› ') + numberWidth + 1 + 1 + markerWidth + 1);
|
|
690
|
+
const questionLines = wrapText(question.question, innerWidth - gutter.length, 2);
|
|
545
691
|
for (const questionLine of questionLines) {
|
|
546
|
-
lines.push(framed([
|
|
692
|
+
lines.push(framed([
|
|
693
|
+
{ text: gutter },
|
|
694
|
+
{ text: questionLine, style: { color: TERMINAL_THEME.text, bold: true } },
|
|
695
|
+
]));
|
|
547
696
|
}
|
|
548
697
|
// The box is sized to the question it renders (capped by the caller's
|
|
549
698
|
// budget). Filling the whole budget left the transcript hidden behind a
|
|
@@ -616,14 +765,19 @@ export class QuestionnaireWidget {
|
|
|
616
765
|
? (chosen ? '[✓]' : '[ ]')
|
|
617
766
|
: (chosen ? '●' : '○');
|
|
618
767
|
const badge = option.recommended ? ' ★ recommended' : '';
|
|
619
|
-
|
|
768
|
+
// Gutter = caret + number + marker, then the content column. The number is
|
|
769
|
+
// padded to the widest index in this question so `10.` does not step one
|
|
770
|
+
// cell right of `1.`: the label column must be identical for every row.
|
|
771
|
+
const numberWidth = String(Math.max(1, options.length)).length;
|
|
772
|
+
const indexLabel = `${index + 1}.`.padStart(numberWidth + 1);
|
|
773
|
+
const prefix = `${highlighted ? '›' : ' '} ${indexLabel} ${marker} `;
|
|
620
774
|
const labelWidth = Math.max(1, innerWidth - displayWidth(prefix) - displayWidth(badge));
|
|
621
775
|
const row = [
|
|
622
776
|
{
|
|
623
777
|
text: highlighted ? '›' : ' ',
|
|
624
778
|
style: highlighted ? { color: TERMINAL_THEME.info, bold: true } : undefined,
|
|
625
779
|
},
|
|
626
|
-
{ text: ` ${
|
|
780
|
+
{ text: ` ${indexLabel} `, style: { color: TERMINAL_THEME.subtle } },
|
|
627
781
|
{
|
|
628
782
|
text: marker,
|
|
629
783
|
style: chosen
|
|
@@ -642,26 +796,29 @@ export class QuestionnaireWidget {
|
|
|
642
796
|
// reading to choose between options. The row budget decides how much
|
|
643
797
|
// of the option LIST is visible; it never cuts the highlighted
|
|
644
798
|
// description short.
|
|
645
|
-
|
|
799
|
+
// Same content column as the label above and the question at the top.
|
|
800
|
+
const descriptionWidth = Math.max(1, innerWidth - displayWidth(prefix));
|
|
646
801
|
for (const line of wrapText(option.description, descriptionWidth, Number.MAX_SAFE_INTEGER)) {
|
|
647
802
|
rows.push(framed([
|
|
648
|
-
{ text: '
|
|
803
|
+
{ text: ' '.repeat(displayWidth(prefix)) },
|
|
649
804
|
{ text: line, style: { color: TERMINAL_THEME.muted, dimColor: true } },
|
|
650
805
|
], TERMINAL_THEME.bgElevated));
|
|
651
806
|
}
|
|
652
807
|
}
|
|
653
808
|
return { rows };
|
|
654
809
|
});
|
|
655
|
-
if (allowOther) {
|
|
810
|
+
if (allowOther && !(allowOptionNotes && question.type === 'select')) {
|
|
656
811
|
const otherIndex = options.length;
|
|
657
812
|
const highlighted = this.optionIndex === otherIndex;
|
|
658
|
-
const
|
|
813
|
+
const numberWidth = String(Math.max(1, otherIndex + 1)).length;
|
|
814
|
+
const indexLabel = `${otherIndex + 1}.`.padStart(numberWidth + 1);
|
|
815
|
+
const prefix = `${highlighted ? '›' : ' '} ${indexLabel} ○ `;
|
|
659
816
|
const rows = [framed([
|
|
660
817
|
{
|
|
661
818
|
text: highlighted ? '›' : ' ',
|
|
662
819
|
style: highlighted ? { color: TERMINAL_THEME.info, bold: true } : undefined,
|
|
663
820
|
},
|
|
664
|
-
{ text: ` ${
|
|
821
|
+
{ text: ` ${indexLabel} `, style: { color: TERMINAL_THEME.subtle } },
|
|
665
822
|
{ text: '○', style: { color: TERMINAL_THEME.muted } },
|
|
666
823
|
{ text: ' ' },
|
|
667
824
|
{
|
|
@@ -670,19 +827,55 @@ export class QuestionnaireWidget {
|
|
|
670
827
|
},
|
|
671
828
|
], highlighted ? TERMINAL_THEME.bgElevated : undefined)];
|
|
672
829
|
if (highlighted) {
|
|
830
|
+
// Same content column as the option labels above.
|
|
673
831
|
rows.push(framed([
|
|
674
|
-
{ text: '
|
|
832
|
+
{ text: ' '.repeat(displayWidth(prefix)) },
|
|
675
833
|
{ text: 'Enter a custom answer.', style: { color: TERMINAL_THEME.muted, dimColor: true } },
|
|
676
834
|
], TERMINAL_THEME.bgElevated));
|
|
677
835
|
}
|
|
678
836
|
blocks.push({ rows });
|
|
679
837
|
}
|
|
838
|
+
const noteEnabled = allowOptionNotes && question.type === 'select';
|
|
839
|
+
const noteEditorRows = noteEnabled
|
|
840
|
+
? bodyRows >= 3 ? Math.min(5, bodyRows - 2)
|
|
841
|
+
: this.editingOptionNote ? bodyRows : 0
|
|
842
|
+
: 0;
|
|
843
|
+
const noteLabelRows = noteEditorRows > 0 && bodyRows >= 3 ? 1 : 0;
|
|
844
|
+
const optionRows = bodyRows - noteEditorRows - noteLabelRows;
|
|
680
845
|
const selectedBlock = Math.min(this.optionIndex, Math.max(0, blocks.length - 1));
|
|
681
|
-
const window = windowOptionBlocks(blocks,
|
|
846
|
+
const window = windowOptionBlocks(blocks, optionRows, selectedBlock);
|
|
682
847
|
body.push(...window.rows);
|
|
683
848
|
if (blocks.length > 0 && (window.start > 0 || window.end < blocks.length - 1)) {
|
|
684
849
|
optionPosition = `option ${selectedBlock + 1}/${blocks.length}`;
|
|
685
850
|
}
|
|
851
|
+
if (noteEditorRows > 0) {
|
|
852
|
+
if (noteLabelRows > 0) {
|
|
853
|
+
body.push(framed([{ text: 'Additional context (optional)', style: { color: TERMINAL_THEME.info, bold: true } }]));
|
|
854
|
+
}
|
|
855
|
+
const note = this.getOptionNote();
|
|
856
|
+
const wrapped = wrapInputSegments(note.segments, note.cursor, innerWidth, 2);
|
|
857
|
+
const firstLine = this.editingOptionNote
|
|
858
|
+
? Math.max(0, wrapped.cursorLine - noteEditorRows + 1)
|
|
859
|
+
: Math.max(0, wrapped.lines.length - noteEditorRows);
|
|
860
|
+
const visible = wrapped.lines.slice(firstLine, firstLine + noteEditorRows);
|
|
861
|
+
const lastLine = firstLine + visible.length;
|
|
862
|
+
visible.forEach((line, index) => {
|
|
863
|
+
const sourceLine = firstLine + index;
|
|
864
|
+
const prefix = sourceLine === 0 ? '❯ '
|
|
865
|
+
: index === 0 && firstLine > 0 ? '↥ '
|
|
866
|
+
: sourceLine === lastLine - 1 && lastLine < wrapped.lines.length ? '↧ ' : ' ';
|
|
867
|
+
const segments = [
|
|
868
|
+
{ text: prefix, style: { color: TERMINAL_THEME.info, bold: true } },
|
|
869
|
+
];
|
|
870
|
+
if (note.segments.length === 0 && !this.editingOptionNote) {
|
|
871
|
+
segments.push({ text: 'Type to add a note', style: { color: TERMINAL_THEME.subtle } });
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
segments.push(...renderLineWithCursorTokens(line, wrapped.cursorCol, this.editingOptionNote && sourceLine === wrapped.cursorLine).map(token => ({ text: token.text, style: token.style })));
|
|
875
|
+
}
|
|
876
|
+
body.push(framed(segments));
|
|
877
|
+
});
|
|
878
|
+
}
|
|
686
879
|
}
|
|
687
880
|
if (spacerRows > 0)
|
|
688
881
|
lines.push(blank());
|
|
@@ -699,6 +892,11 @@ export class QuestionnaireWidget {
|
|
|
699
892
|
else if (question.type === 'text') {
|
|
700
893
|
hint = `${isLast ? 'Enter submit' : 'Enter next'} · ←/→ questions · Esc cancel`;
|
|
701
894
|
}
|
|
895
|
+
else if (allowOptionNotes && question.type === 'select') {
|
|
896
|
+
hint = this.editingOptionNote
|
|
897
|
+
? `Enter ${isLast ? 'submit' : 'next'} · Shift+Enter newline · Esc options`
|
|
898
|
+
: `↑↓ options · Type note · Enter ${isLast ? 'submit' : 'next'} · Esc cancel`;
|
|
899
|
+
}
|
|
702
900
|
else if (question.type === 'multiselect') {
|
|
703
901
|
hint = `↑↓ move · Space toggle · ${isLast ? 'Enter submit' : 'Enter next'} · Esc cancel`;
|
|
704
902
|
}
|
|
@@ -88,6 +88,15 @@ export declare class ResumePanelWidget {
|
|
|
88
88
|
private previewViewportRows;
|
|
89
89
|
private lastBodyRows;
|
|
90
90
|
private lastInnerWidth;
|
|
91
|
+
/**
|
|
92
|
+
* Column the painted frame starts on, in surface coordinates.
|
|
93
|
+
*
|
|
94
|
+
* Zero unless the terminal is wide enough to trigger the shared cap, in which
|
|
95
|
+
* case it is that centring margin. The wheel bands add it back: a notch lands
|
|
96
|
+
* in terminal columns, so without it a capped page would scroll from a pane
|
|
97
|
+
* that is no longer under the pointer.
|
|
98
|
+
*/
|
|
99
|
+
private frameInset;
|
|
91
100
|
/** Pane geometry from the last paint; see `panelSplit()`. */
|
|
92
101
|
private lastSplit;
|
|
93
102
|
private loading;
|
|
@@ -14,12 +14,11 @@
|
|
|
14
14
|
* row: moving the selection reloads the transcript preview, and the row itself
|
|
15
15
|
* grows a one-line snippet under it.
|
|
16
16
|
*/
|
|
17
|
-
import { box, text } from '../core/index.js';
|
|
18
17
|
import { displayWidth, padEndDisplay } from '../core/text_width.js';
|
|
19
18
|
import { TERMINAL_THEME } from '../views/theme.js';
|
|
20
19
|
import { fuzzyMatch, rankFuzzy } from '../../utils/fuzzy_match.js';
|
|
21
20
|
import { buildTranscriptLines, messageLines as renderMessageLines, previewSnippet, transcriptHeaderText, windowedLines, TRANSCRIPT_PANE_MIN_BODY_ROWS, TRANSCRIPT_PANE_MIN_WIDTH, } from './transcript_preview.js';
|
|
22
|
-
import { bodyBand, bottomBorder, clipText, dropLastGrapheme, fitSegments, formatRelativeTime, framedLine, isInsideBand, joinColumns, lineText, planPanelSplit, sideBySideBands, stackedBands, topBorder, WHEEL_LINES, } from './panel_frame.js';
|
|
21
|
+
import { bodyBand, bottomBorder, clipText, dropLastGrapheme, fitSegments, formatRelativeTime, framedLine, isInsideBand, joinColumns, lineText, planPanelSplit, sideBySideBands, stackedBands, topBorder, WHEEL_LINES, buildWidgetFrameView, widgetFrameGeometry, } from './panel_frame.js';
|
|
23
22
|
export { formatRelativeTime };
|
|
24
23
|
const TAB_LABELS = {
|
|
25
24
|
workspace: 'This workspace',
|
|
@@ -61,6 +60,15 @@ export class ResumePanelWidget {
|
|
|
61
60
|
previewViewportRows = 1;
|
|
62
61
|
lastBodyRows = 1;
|
|
63
62
|
lastInnerWidth = 78;
|
|
63
|
+
/**
|
|
64
|
+
* Column the painted frame starts on, in surface coordinates.
|
|
65
|
+
*
|
|
66
|
+
* Zero unless the terminal is wide enough to trigger the shared cap, in which
|
|
67
|
+
* case it is that centring margin. The wheel bands add it back: a notch lands
|
|
68
|
+
* in terminal columns, so without it a capped page would scroll from a pane
|
|
69
|
+
* that is no longer under the pointer.
|
|
70
|
+
*/
|
|
71
|
+
frameInset = 0;
|
|
64
72
|
/** Pane geometry from the last paint; see `panelSplit()`. */
|
|
65
73
|
lastSplit = null;
|
|
66
74
|
loading = false;
|
|
@@ -661,6 +669,8 @@ export class ResumePanelWidget {
|
|
|
661
669
|
scroll: this.previewScroll,
|
|
662
670
|
viewportRows: this.previewViewportRows,
|
|
663
671
|
focused: this.focus === 'preview',
|
|
672
|
+
// One cell of the heading's column is the gutter between the two panes.
|
|
673
|
+
maxWidth: Math.max(8, this.previewPaneWidth() - 1),
|
|
664
674
|
});
|
|
665
675
|
}
|
|
666
676
|
/** The bubble list for the highlighted row, cached by (row, width, status). */
|
|
@@ -742,17 +752,7 @@ export class ResumePanelWidget {
|
|
|
742
752
|
const lines = this.buildSurfaceLines(cols, maxRows);
|
|
743
753
|
return {
|
|
744
754
|
content: lines.map(lineText).join('\n'),
|
|
745
|
-
view:
|
|
746
|
-
flexDirection: 'row',
|
|
747
|
-
height: 1,
|
|
748
|
-
flexShrink: 0,
|
|
749
|
-
backgroundColor: line.backgroundColor,
|
|
750
|
-
})), {
|
|
751
|
-
flexDirection: 'column',
|
|
752
|
-
height: lines.length,
|
|
753
|
-
flexShrink: 0,
|
|
754
|
-
backgroundColor: TERMINAL_THEME.bg,
|
|
755
|
-
}),
|
|
755
|
+
view: buildWidgetFrameView(lines, this.frameInset),
|
|
756
756
|
lineCount: lines.length,
|
|
757
757
|
};
|
|
758
758
|
}
|
|
@@ -768,7 +768,8 @@ export class ResumePanelWidget {
|
|
|
768
768
|
// The host embeds the surface in a `paddingX: 1` column, so `cols - 2` is
|
|
769
769
|
// the full usable width. No cap: the transcript pane should absorb every
|
|
770
770
|
// extra column instead of leaving dead space on wide terminals.
|
|
771
|
-
const width =
|
|
771
|
+
const { width, inset } = widgetFrameGeometry(termCols);
|
|
772
|
+
this.frameInset = inset;
|
|
772
773
|
const height = Math.max(1, Math.floor(maxRows ?? 20));
|
|
773
774
|
const innerWidth = Math.max(0, width - 4);
|
|
774
775
|
this.lastInnerWidth = innerWidth;
|
|
@@ -809,7 +810,7 @@ export class ResumePanelWidget {
|
|
|
809
810
|
lines.push(framedLine(row ? row.segments : [], width));
|
|
810
811
|
}
|
|
811
812
|
// No transcript pane at this size, so only the list is a wheel target.
|
|
812
|
-
this.listBand = bodyBand(listTop, lines.length - 1, innerWidth);
|
|
813
|
+
this.listBand = bodyBand(listTop, lines.length - 1, innerWidth, this.frameInset);
|
|
813
814
|
this.previewBand = null;
|
|
814
815
|
}
|
|
815
816
|
else if (split.placement === 'bottom') {
|
|
@@ -846,7 +847,7 @@ export class ResumePanelWidget {
|
|
|
846
847
|
lines.push(framedLine(previewRow ? previewRow.segments : [], width));
|
|
847
848
|
}
|
|
848
849
|
// Stacked, the panes share the width and are told apart by rows.
|
|
849
|
-
const bands = stackedBands({ top: stackedListTop, bottom: stackedListBottom }, { top: stackedPreviewTop, bottom: lines.length - 1 }, innerWidth);
|
|
850
|
+
const bands = stackedBands({ top: stackedListTop, bottom: stackedListBottom }, { top: stackedPreviewTop, bottom: lines.length - 1 }, innerWidth, this.frameInset);
|
|
850
851
|
this.listBand = bands.list;
|
|
851
852
|
this.previewBand = bands.preview;
|
|
852
853
|
}
|
|
@@ -888,7 +889,7 @@ export class ResumePanelWidget {
|
|
|
888
889
|
], width));
|
|
889
890
|
}
|
|
890
891
|
// Both panes start at the shared heading row and differ only in columns.
|
|
891
|
-
const bands = sideBySideBands(sideBandTop, lines.length - 1, listWidth, paneWidth);
|
|
892
|
+
const bands = sideBySideBands(sideBandTop, lines.length - 1, listWidth, paneWidth, this.frameInset);
|
|
892
893
|
this.listBand = bands.list;
|
|
893
894
|
this.previewBand = bands.preview;
|
|
894
895
|
}
|
|
@@ -37,6 +37,15 @@ export interface RewindPanelSurface {
|
|
|
37
37
|
export declare class RewindPanelWidget {
|
|
38
38
|
private host;
|
|
39
39
|
private active;
|
|
40
|
+
/**
|
|
41
|
+
* Column the painted frame starts on, in surface coordinates.
|
|
42
|
+
*
|
|
43
|
+
* Zero unless the terminal is wide enough to trigger the shared cap, in which
|
|
44
|
+
* case it is that centring margin. The wheel bands add it back: a notch lands
|
|
45
|
+
* in terminal columns, so without it a capped page would scroll from a pane
|
|
46
|
+
* that is no longer under the pointer.
|
|
47
|
+
*/
|
|
48
|
+
private frameInset;
|
|
40
49
|
private selectedIndex;
|
|
41
50
|
private focus;
|
|
42
51
|
/** First preview line to draw; only read when `previewAtTail` is false. */
|
|
@@ -18,10 +18,9 @@
|
|
|
18
18
|
* handling, and renders a framed surface. Only the visible window of prompts is
|
|
19
19
|
* built, and the selection always stays inside it.
|
|
20
20
|
*/
|
|
21
|
-
import { box, text } from '../core/index.js';
|
|
22
21
|
import { padEndDisplay } from '../core/text_width.js';
|
|
23
22
|
import { TERMINAL_THEME } from '../views/theme.js';
|
|
24
|
-
import { bottomBorder, clipText, fitSegments, formatRelativeTime, framedLine, isInsideBand, joinColumns, lineText, planPanelSplit, sideBySideBands, stackedBands, topBorder, WHEEL_LINES, } from './panel_frame.js';
|
|
23
|
+
import { bottomBorder, clipText, fitSegments, formatRelativeTime, framedLine, isInsideBand, joinColumns, lineText, planPanelSplit, sideBySideBands, stackedBands, topBorder, WHEEL_LINES, buildWidgetFrameView, widgetFrameGeometry, } from './panel_frame.js';
|
|
25
24
|
import { buildBubbleLines } from './transcript_preview.js';
|
|
26
25
|
/** Rows each entry claims in the list (prompt line + detail line). */
|
|
27
26
|
const ENTRY_ROWS = 2;
|
|
@@ -65,6 +64,15 @@ function isCtrlKey(input, key, letter) {
|
|
|
65
64
|
export class RewindPanelWidget {
|
|
66
65
|
host;
|
|
67
66
|
active = null;
|
|
67
|
+
/**
|
|
68
|
+
* Column the painted frame starts on, in surface coordinates.
|
|
69
|
+
*
|
|
70
|
+
* Zero unless the terminal is wide enough to trigger the shared cap, in which
|
|
71
|
+
* case it is that centring margin. The wheel bands add it back: a notch lands
|
|
72
|
+
* in terminal columns, so without it a capped page would scroll from a pane
|
|
73
|
+
* that is no longer under the pointer.
|
|
74
|
+
*/
|
|
75
|
+
frameInset = 0;
|
|
68
76
|
selectedIndex = 0;
|
|
69
77
|
focus = 'list';
|
|
70
78
|
/** First preview line to draw; only read when `previewAtTail` is false. */
|
|
@@ -264,17 +272,7 @@ export class RewindPanelWidget {
|
|
|
264
272
|
const lines = this.buildSurfaceLines(cols, maxRows);
|
|
265
273
|
return {
|
|
266
274
|
content: lines.map(lineText).join('\n'),
|
|
267
|
-
view:
|
|
268
|
-
flexDirection: 'row',
|
|
269
|
-
height: 1,
|
|
270
|
-
flexShrink: 0,
|
|
271
|
-
backgroundColor: line.backgroundColor,
|
|
272
|
-
})), {
|
|
273
|
-
flexDirection: 'column',
|
|
274
|
-
height: lines.length,
|
|
275
|
-
flexShrink: 0,
|
|
276
|
-
backgroundColor: TERMINAL_THEME.bg,
|
|
277
|
-
}),
|
|
275
|
+
view: buildWidgetFrameView(lines, this.frameInset),
|
|
278
276
|
lineCount: lines.length,
|
|
279
277
|
};
|
|
280
278
|
}
|
|
@@ -289,7 +287,8 @@ export class RewindPanelWidget {
|
|
|
289
287
|
const termCols = Math.max(1, Math.floor(cols || process.stdout.columns || 80));
|
|
290
288
|
// The host embeds the surface in a `paddingX: 1` column, so `cols - 2` is
|
|
291
289
|
// the usable width, same convention as the resume panel.
|
|
292
|
-
const width =
|
|
290
|
+
const { width, inset } = widgetFrameGeometry(termCols);
|
|
291
|
+
this.frameInset = inset;
|
|
293
292
|
const height = Math.max(1, Math.floor(maxRows ?? 20));
|
|
294
293
|
const innerWidth = Math.max(0, width - 4);
|
|
295
294
|
const nowMs = this.active.now ? this.active.now() : Date.now();
|
|
@@ -353,7 +352,7 @@ export class RewindPanelWidget {
|
|
|
353
352
|
...fitSegments(previewRow?.segments ?? [], previewWidth),
|
|
354
353
|
], width));
|
|
355
354
|
}
|
|
356
|
-
this.paneBands = sideBySideBands(sideBandTop, lines.length - 1, split.listWidth, previewWidth);
|
|
355
|
+
this.paneBands = sideBySideBands(sideBandTop, lines.length - 1, split.listWidth, previewWidth, this.frameInset);
|
|
357
356
|
}
|
|
358
357
|
else if (split.placement === 'bottom') {
|
|
359
358
|
this.pushPaneHeader(lines, width, [{
|
|
@@ -377,7 +376,7 @@ export class RewindPanelWidget {
|
|
|
377
376
|
for (let index = 0; index < split.previewRows; index += 1) {
|
|
378
377
|
lines.push(framedLine(previewRows[index]?.segments ?? [], width));
|
|
379
378
|
}
|
|
380
|
-
this.paneBands = stackedBands({ top: stackedListTop, bottom: stackedListBottom }, { top: stackedPreviewTop, bottom: lines.length - 1 }, innerWidth);
|
|
379
|
+
this.paneBands = stackedBands({ top: stackedListTop, bottom: stackedListBottom }, { top: stackedPreviewTop, bottom: lines.length - 1 }, innerWidth, this.frameInset);
|
|
381
380
|
}
|
|
382
381
|
else {
|
|
383
382
|
this.pushPaneHeader(lines, width, [{
|
|
@@ -30,6 +30,12 @@ export declare class SelectWidget {
|
|
|
30
30
|
private checked;
|
|
31
31
|
private otherMode;
|
|
32
32
|
private otherValue;
|
|
33
|
+
/**
|
|
34
|
+
* Columns the option gutter claims before the label: the `❯ `/` ` pointer
|
|
35
|
+
* plus the state marker and its separator. Every body row (question, label,
|
|
36
|
+
* description, Other hint) is indented by this, so the card has one left edge.
|
|
37
|
+
*/
|
|
38
|
+
private static labelColumn;
|
|
33
39
|
constructor(host: WidgetHost);
|
|
34
40
|
/**
|
|
35
41
|
* Returns true if the option at the given index can be selected/activated.
|