@readwise/cli 0.3.1 → 0.5.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.
package/dist/tui/app.js CHANGED
@@ -125,6 +125,45 @@ function defaultFormCursor(fields, filtered, values) {
125
125
  const firstBlank = filtered.findIndex((idx) => idx >= 0 && missing.has(fields[idx].name));
126
126
  return firstBlank >= 0 ? firstBlank : executeIndex(filtered);
127
127
  }
128
+ function classifyField(prop) {
129
+ if (isArrayOfObjects(prop))
130
+ return "arrayObj";
131
+ if (dateFieldFormat(prop))
132
+ return "date";
133
+ const eVals = prop.enum || prop.items?.enum;
134
+ if (prop.type === "boolean")
135
+ return "bool";
136
+ if (eVals && prop.type === "array")
137
+ return "arrayEnum";
138
+ if (eVals)
139
+ return "enum";
140
+ if (prop.type === "array")
141
+ return "arrayText";
142
+ return "text";
143
+ }
144
+ function fieldTypeBadge(prop) {
145
+ const badges = {
146
+ arrayObj: "form", date: "date", bool: "yes/no", arrayEnum: "multi",
147
+ enum: "select", arrayText: "list", text: "text",
148
+ };
149
+ const badge = badges[classifyField(prop)];
150
+ if (badge !== "text")
151
+ return badge;
152
+ if (prop.type === "integer" || prop.type === "number")
153
+ return "number";
154
+ return "text";
155
+ }
156
+ function footerForFieldKind(kind) {
157
+ switch (kind) {
158
+ case "arrayObj": return "\u2191\u2193 navigate \u00B7 enter add/edit \u00B7 backspace delete \u00B7 esc back";
159
+ case "date": return "\u2190\u2192 part \u00B7 \u2191\u2193 adjust \u00B7 t today \u00B7 enter confirm \u00B7 esc cancel";
160
+ case "arrayEnum": return "space toggle \u00B7 enter confirm \u00B7 esc cancel";
161
+ case "arrayText": return "\u2191\u2193 navigate \u00B7 enter add/edit \u00B7 backspace delete \u00B7 esc confirm";
162
+ case "enum":
163
+ case "bool": return "\u2191\u2193 navigate \u00B7 enter confirm \u00B7 esc cancel";
164
+ case "text": return "enter confirm \u00B7 esc cancel";
165
+ }
166
+ }
128
167
  function formFieldValueDisplay(value, maxWidth) {
129
168
  if (!value)
130
169
  return style.dim("–");
@@ -205,8 +244,9 @@ function popFormStack(state) {
205
244
  formFilteredIndices: parentFiltered,
206
245
  formListCursor: defaultFormCursor(entry.parentFields, parentFiltered, newParentValues),
207
246
  formScrollTop: 0,
208
- formShowRequired: false,
247
+ formShowRequired: false, formShowOptional: false,
209
248
  formInputBuf: "",
249
+ formInputCursorPos: 0,
210
250
  };
211
251
  }
212
252
  const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -248,6 +288,44 @@ function wrapText(text, width) {
248
288
  lines.push(current);
249
289
  return lines.length > 0 ? lines : [""];
250
290
  }
291
+ // --- Word boundary helpers ---
292
+ function prevWordBoundary(buf, pos) {
293
+ if (pos <= 0)
294
+ return 0;
295
+ let i = pos - 1;
296
+ // Skip whitespace
297
+ while (i > 0 && /\s/.test(buf[i]))
298
+ i--;
299
+ // Skip word chars or non-word non-space chars
300
+ if (i >= 0 && /\w/.test(buf[i])) {
301
+ while (i > 0 && /\w/.test(buf[i - 1]))
302
+ i--;
303
+ }
304
+ else {
305
+ while (i > 0 && !/\w/.test(buf[i - 1]) && !/\s/.test(buf[i - 1]))
306
+ i--;
307
+ }
308
+ return i;
309
+ }
310
+ function nextWordBoundary(buf, pos) {
311
+ const len = buf.length;
312
+ if (pos >= len)
313
+ return len;
314
+ let i = pos;
315
+ // Skip current word chars or non-word non-space chars
316
+ if (/\w/.test(buf[i])) {
317
+ while (i < len && /\w/.test(buf[i]))
318
+ i++;
319
+ }
320
+ else if (!/\s/.test(buf[i])) {
321
+ while (i < len && !/\w/.test(buf[i]) && !/\s/.test(buf[i]))
322
+ i++;
323
+ }
324
+ // Skip whitespace
325
+ while (i < len && /\s/.test(buf[i]))
326
+ i++;
327
+ return i;
328
+ }
251
329
  function dateFieldFormat(prop) {
252
330
  if (prop.format === "date")
253
331
  return "date";
@@ -385,7 +463,7 @@ function renderCommandList(state) {
385
463
  content.push(` ${logoLine} ${style.boldYellow("Readwise")} ${style.dim("v" + VERSION)}`);
386
464
  }
387
465
  else if (i === Math.floor(LOGO.length / 2)) {
388
- content.push(` ${logoLine} ${style.dim("Command-line interface")}`);
466
+ content.push(` ${logoLine} ${style.dim("Built for AI agents · This TUI is just for fun/learning")}`);
389
467
  }
390
468
  else {
391
469
  content.push(` ${logoLine}`);
@@ -445,8 +523,8 @@ function renderCommandList(state) {
445
523
  }
446
524
  }
447
525
  const footer = state.quitConfirm
448
- ? style.yellow("Press q or esc again to quit")
449
- : style.dim("type to search ↑↓ navigate enter select esc clear/quit");
526
+ ? style.yellow("Press again to quit")
527
+ : style.dim("type to search · ↑↓ navigate · enter select · esc/ctrl+c quit");
450
528
  return renderLayout({
451
529
  breadcrumb: style.boldYellow("Readwise"),
452
530
  content,
@@ -458,15 +536,227 @@ function renderForm(state) {
458
536
  const tool = state.selectedTool;
459
537
  const fields = state.fields;
460
538
  const toolTitle = humanLabel(tool.name, toolPrefix(tool));
461
- // Build title: tool name + any stack breadcrumb
462
- const stackParts = state.formStack.map((e) => e.parentFieldName);
463
- const title = stackParts.length > 0
464
- ? toolTitle + " › " + stackParts.join(" › ")
465
- : toolTitle;
466
- if (state.formEditing && state.formEditFieldIdx >= 0) {
467
- return renderFormEditMode(state, title, fields, contentHeight, innerWidth);
468
- }
469
- return renderFormPaletteMode(state, title, fields, contentHeight, innerWidth);
539
+ // Sub-forms (nested array-of-objects) keep existing form UI
540
+ if (state.formStack.length > 0) {
541
+ const stackParts = state.formStack.map((e) => e.parentFieldName);
542
+ const title = toolTitle + " › " + stackParts.join(" › ");
543
+ if (state.formEditing && state.formEditFieldIdx >= 0) {
544
+ return renderFormEditMode(state, title, fields, contentHeight, innerWidth);
545
+ }
546
+ return renderFormPaletteMode(state, title, fields, contentHeight, innerWidth);
547
+ }
548
+ // Top-level: command builder
549
+ return renderCommandBuilder(state, toolTitle, fields, contentHeight, innerWidth);
550
+ }
551
+ function renderCommandBuilder(state, title, fields, contentHeight, innerWidth) {
552
+ const tool = state.selectedTool;
553
+ const cmdName = tool.name.replace(/_/g, "-");
554
+ const content = [];
555
+ const editField = state.formEditing && state.formEditFieldIdx >= 0
556
+ ? fields[state.formEditFieldIdx]
557
+ : null;
558
+ // Header
559
+ content.push("");
560
+ content.push(" " + style.bold(title));
561
+ if (tool.description) {
562
+ const wrapped = wrapText(tool.description, innerWidth - 4);
563
+ for (const line of wrapped) {
564
+ content.push(" " + style.dim(line));
565
+ }
566
+ }
567
+ content.push("");
568
+ // Classify editing field type once for both content and footer
569
+ const editKind = editField ? classifyField(editField.prop) : null;
570
+ const isTextLikeEdit = editKind === "text";
571
+ // Build command lines
572
+ const argLines = [];
573
+ for (const field of fields) {
574
+ const flagName = field.name.replace(/_/g, "-");
575
+ if (field === editField) {
576
+ if (isTextLikeEdit) {
577
+ // Inline cursor for text fields
578
+ const buf = state.formInputBuf;
579
+ const before = buf.slice(0, state.formInputCursorPos);
580
+ const cursorChar = state.formInputCursorPos < buf.length ? buf[state.formInputCursorPos] : " ";
581
+ const after = state.formInputCursorPos < buf.length ? buf.slice(state.formInputCursorPos + 1) : "";
582
+ argLines.push(" --" + flagName + "=" + style.cyan(before) + style.inverse(cursorChar) + style.cyan(after));
583
+ }
584
+ else if (isArrayOfObjects(field.prop)) {
585
+ // Array-of-objects: show item count
586
+ const existing = state.formValues[field.name] || "[]";
587
+ let items = [];
588
+ try {
589
+ items = JSON.parse(existing);
590
+ }
591
+ catch { /* */ }
592
+ const label = items.length > 0 ? `[${items.length} item${items.length > 1 ? "s" : ""}]` : "[...]";
593
+ argLines.push(" --" + flagName + "=" + style.yellow(label));
594
+ }
595
+ else {
596
+ // Non-text editors (enum, bool, date): show pending
597
+ argLines.push(" --" + flagName + "=" + style.inverse(" "));
598
+ }
599
+ }
600
+ else {
601
+ const val = state.formValues[field.name];
602
+ if (val) {
603
+ const needsQuotes = val.includes(" ") || val.includes(",");
604
+ const displayVal = needsQuotes ? '"' + val + '"' : val;
605
+ argLines.push(" --" + flagName + "=" + style.cyan(displayVal));
606
+ }
607
+ }
608
+ }
609
+ // Render command with line continuations
610
+ const cmdPrefix = " " + style.dim("$") + " " + style.dim("readwise") + " " + cmdName;
611
+ if (argLines.length === 0) {
612
+ content.push(cmdPrefix);
613
+ }
614
+ else {
615
+ content.push(cmdPrefix + " \\");
616
+ for (let i = 0; i < argLines.length; i++) {
617
+ const isLast = i === argLines.length - 1;
618
+ content.push(argLines[i] + (isLast ? "" : " \\"));
619
+ }
620
+ }
621
+ content.push("");
622
+ // Context area: field description, editor, or ready state
623
+ if (editField) {
624
+ // Field description
625
+ if (editField.prop.description) {
626
+ content.push(" " + style.dim(editField.prop.description));
627
+ }
628
+ if (editField.prop.examples?.length) {
629
+ const exStr = editField.prop.examples.map((e) => typeof e === "string" ? e : JSON.stringify(e)).join(", ");
630
+ content.push(" " + style.dim("e.g. ") + style.cyan(truncateVisible(exStr, innerWidth - 10)));
631
+ }
632
+ if (editField.prop.default != null) {
633
+ content.push(" " + style.dim("default: " + editField.prop.default));
634
+ }
635
+ const eVals = editField.prop.enum || editField.prop.items?.enum;
636
+ if (editKind === "bool") {
637
+ content.push("");
638
+ const choices = ["true", "false"];
639
+ for (let ci = 0; ci < choices.length; ci++) {
640
+ const sel = ci === state.formEnumCursor;
641
+ content.push(sel ? " " + style.cyan(style.bold("\u203A " + choices[ci])) : " " + choices[ci]);
642
+ }
643
+ }
644
+ else if (editKind === "enum" && eVals) {
645
+ content.push("");
646
+ for (let ci = 0; ci < eVals.length; ci++) {
647
+ const sel = ci === state.formEnumCursor;
648
+ content.push(sel ? " " + style.cyan(style.bold("\u203A " + eVals[ci])) : " " + eVals[ci]);
649
+ }
650
+ }
651
+ else if (editKind === "arrayEnum" && eVals) {
652
+ content.push("");
653
+ for (let ci = 0; ci < eVals.length; ci++) {
654
+ const sel = ci === state.formEnumCursor;
655
+ const checked = state.formEnumSelected.has(ci);
656
+ const check = checked ? style.cyan("[x]") : style.dim("[ ]");
657
+ content.push((sel ? " \u203A " : " ") + check + " " + eVals[ci]);
658
+ }
659
+ }
660
+ else if (editKind === "date") {
661
+ const dateFmt = dateFieldFormat(editField.prop);
662
+ content.push("");
663
+ content.push(" " + renderDateParts(state.dateParts, state.datePartCursor, dateFmt));
664
+ }
665
+ else if (editKind === "arrayText") {
666
+ const existing = state.formValues[editField.name] || "";
667
+ const items = existing ? existing.split(",").map((s) => s.trim()).filter(Boolean) : [];
668
+ content.push("");
669
+ for (let i = 0; i < items.length; i++) {
670
+ const isCursor = i === state.formEnumCursor;
671
+ content.push((isCursor ? " \u276F " : " ") + style.cyan(items[i]));
672
+ }
673
+ const onInput = state.formEnumCursor === items.length;
674
+ content.push((onInput ? " \u276F " : " ") + style.cyan(state.formInputBuf) + (onInput ? style.inverse(" ") : ""));
675
+ }
676
+ else if (editKind === "arrayObj") {
677
+ const existing = state.formValues[editField.name] || "[]";
678
+ let items = [];
679
+ try {
680
+ items = JSON.parse(existing);
681
+ }
682
+ catch { /* */ }
683
+ content.push("");
684
+ for (let i = 0; i < items.length; i++) {
685
+ const item = items[i];
686
+ const summary = Object.entries(item)
687
+ .filter(([, v]) => v != null && v !== "")
688
+ .map(([k, v]) => `${k}: ${typeof v === "string" ? v : JSON.stringify(v)}`)
689
+ .join(", ");
690
+ const isCursor = i === state.formEnumCursor;
691
+ content.push((isCursor ? " \u276F " : " ") + truncateVisible(summary || "(empty)", innerWidth - 6));
692
+ }
693
+ if (items.length > 0)
694
+ content.push("");
695
+ const addCursor = state.formEnumCursor === items.length;
696
+ content.push(addCursor
697
+ ? " " + style.inverse(style.green(" + Add new item "))
698
+ : " " + style.dim("+") + " Add new item");
699
+ }
700
+ else if (isTextLikeEdit) {
701
+ // Text field: cursor is already inline in the command string
702
+ // Just show a hint if empty
703
+ if (!state.formInputBuf) {
704
+ content.push("");
705
+ content.push(" " + style.dim("Type a value and press enter"));
706
+ }
707
+ }
708
+ }
709
+ else {
710
+ // Ready state / optional picker
711
+ const missing = missingRequiredFields(fields, state.formValues);
712
+ if (missing.length > 0) {
713
+ content.push(" " + style.red("Missing: " + missing.map((f) => f.name).join(", ")));
714
+ }
715
+ else {
716
+ content.push(" " + style.dim("Press enter to run"));
717
+ }
718
+ // Always show optional params
719
+ const optionalFields = fields
720
+ .map((f, i) => ({ field: f, idx: i }))
721
+ .filter(({ field }) => !field.required);
722
+ if (optionalFields.length > 0) {
723
+ content.push("");
724
+ content.push(" " + style.dim("Optional parameters (tab to add)"));
725
+ content.push("");
726
+ const maxFlagWidth = Math.max(...optionalFields.map(({ field }) => field.name.length), 0) + 2;
727
+ for (let i = 0; i < optionalFields.length; i++) {
728
+ const { field } = optionalFields[i];
729
+ const flagName = field.name.replace(/_/g, "-");
730
+ const hasValue = !!state.formValues[field.name]?.trim();
731
+ const sel = state.formShowOptional && i === state.formListCursor;
732
+ const prefix = sel ? " \u276F " : " ";
733
+ const paddedName = flagName.padEnd(maxFlagWidth);
734
+ const desc = field.prop.description
735
+ ? style.dim(truncateVisible(field.prop.description, innerWidth - maxFlagWidth - 8))
736
+ : "";
737
+ if (sel) {
738
+ content.push(style.boldYellow(prefix + paddedName) + " " + desc);
739
+ }
740
+ else if (hasValue) {
741
+ content.push(prefix + style.green(paddedName) + " " + desc);
742
+ }
743
+ else {
744
+ content.push(prefix + style.dim(paddedName) + " " + desc);
745
+ }
746
+ }
747
+ }
748
+ }
749
+ // Footer
750
+ const footer = editKind
751
+ ? style.dim(footerForFieldKind(editKind))
752
+ : state.formShowOptional
753
+ ? style.dim("\u2191\u2193 select \u00B7 enter add \u00B7 esc done")
754
+ : style.dim("enter run \u00B7 tab add option \u00B7 esc back");
755
+ return renderLayout({
756
+ breadcrumb: style.boldYellow("Readwise") + style.dim(" \u203A ") + style.bold(title),
757
+ content,
758
+ footer,
759
+ });
470
760
  }
471
761
  function renderFormPaletteMode(state, title, fields, contentHeight, innerWidth) {
472
762
  const content = [];
@@ -485,51 +775,117 @@ function renderFormPaletteMode(state, title, fields, contentHeight, innerWidth)
485
775
  content.push(" " + style.dim(line));
486
776
  }
487
777
  }
778
+ // Progress indicator for required fields
779
+ const requiredFields = fields.filter((f) => f.required);
780
+ if (requiredFields.length > 0) {
781
+ const filledRequired = requiredFields.filter((f) => {
782
+ const val = state.formValues[f.name]?.trim();
783
+ if (!val)
784
+ return false;
785
+ if (isArrayOfObjects(f.prop)) {
786
+ try {
787
+ return JSON.parse(val).length > 0;
788
+ }
789
+ catch {
790
+ return false;
791
+ }
792
+ }
793
+ return true;
794
+ });
795
+ const allFilled = filledRequired.length === requiredFields.length;
796
+ const progressText = `${filledRequired.length} of ${requiredFields.length} required`;
797
+ content.push(" " + (allFilled ? style.green("✓ " + progressText) : style.dim(progressText)));
798
+ }
488
799
  content.push("");
489
- // Search input
800
+ // Search input (only show when there's a search query or many fields)
490
801
  const queryText = state.formSearchQuery;
491
- const before = queryText.slice(0, state.formSearchCursorPos);
492
- const cursorChar = state.formSearchCursorPos < queryText.length
493
- ? queryText[state.formSearchCursorPos]
494
- : " ";
495
- const after = state.formSearchCursorPos < queryText.length
496
- ? queryText.slice(state.formSearchCursorPos + 1)
497
- : "";
498
- content.push(" " + style.yellow("❯") + " " + before + style.inverse(cursorChar) + after);
499
- content.push("");
500
- // Compute maxLabelWidth
802
+ if (queryText || fields.length > 6) {
803
+ const before = queryText.slice(0, state.formSearchCursorPos);
804
+ const cursorChar = state.formSearchCursorPos < queryText.length
805
+ ? queryText[state.formSearchCursorPos]
806
+ : " ";
807
+ const after = state.formSearchCursorPos < queryText.length
808
+ ? queryText.slice(state.formSearchCursorPos + 1)
809
+ : "";
810
+ content.push(" " + style.dim("/") + " " + before + style.inverse(cursorChar) + after);
811
+ content.push("");
812
+ }
813
+ else {
814
+ content.push("");
815
+ }
816
+ // Compute maxLabelWidth (include " *" for required fields)
501
817
  const maxLabelWidth = Math.max(...fields.map((f) => f.name.length + (f.required ? 2 : 0)), 6) + 1;
502
- // Value display width budget: innerWidth - prefix(3) - label - gap(2)
503
- const valueAvail = Math.max(0, innerWidth - 3 - maxLabelWidth - 2);
818
+ // Badge width: " text" = ~7 chars max
819
+ const badgeWidth = 8;
820
+ // Value display width budget: innerWidth - cursor(3) - label - gap(2) - badge
821
+ const valueAvail = Math.max(0, innerWidth - 3 - maxLabelWidth - 2 - badgeWidth);
504
822
  const headerUsed = content.length;
505
823
  // Reserve space for: blank + Execute + blank + description (up to 4 lines)
506
824
  const listHeight = Math.max(1, contentHeight - headerUsed - 8);
507
825
  const filtered = state.formFilteredIndices;
508
826
  const hasOnlyExecute = filtered.length === 1 && filtered[0] === -1;
827
+ // Split fields into required and optional
828
+ const requiredIdxs = filtered.filter((idx) => idx >= 0 && idx < fields.length && fields[idx].required);
829
+ const optionalIdxs = filtered.filter((idx) => idx >= 0 && idx < fields.length && !fields[idx].required);
830
+ const hasOptional = optionalIdxs.length > 0;
831
+ const showingOptional = state.formShowOptional || state.formSearchQuery;
832
+ // Count optional fields that have been filled
833
+ const filledOptionalCount = optionalIdxs.filter((idx) => !!state.formValues[fields[idx].name]?.trim()).length;
834
+ const renderField = (fieldIdx) => {
835
+ const field = fields[fieldIdx];
836
+ const val = state.formValues[field.name] || "";
837
+ const isFilled = !!val.trim();
838
+ const listPos = filtered.indexOf(fieldIdx);
839
+ const selected = listPos === state.formListCursor;
840
+ // Value display
841
+ const valStr = formFieldValueDisplay(val, valueAvail);
842
+ // Type badge
843
+ const badge = style.dim(fieldTypeBadge(field.prop));
844
+ const cursor = selected ? " ❯ " : " ";
845
+ if (selected) {
846
+ const label = field.name + (field.required ? " *" : "");
847
+ content.push(style.boldYellow(cursor) + style.boldYellow(label.padEnd(maxLabelWidth)) + " " + valStr + " " + badge);
848
+ }
849
+ else if (isFilled) {
850
+ const label = field.name + (field.required ? " *" : "");
851
+ content.push(cursor + style.green(label.padEnd(maxLabelWidth)) + " " + valStr + " " + badge);
852
+ }
853
+ else {
854
+ // Unfilled: show required * in red
855
+ const namePart = field.name;
856
+ const starPart = field.required ? " *" : "";
857
+ const plainLabel = namePart + starPart;
858
+ const padAmount = Math.max(0, maxLabelWidth - plainLabel.length);
859
+ const displayLabel = field.required ? namePart + style.red(" *") + " ".repeat(padAmount) : plainLabel.padEnd(maxLabelWidth);
860
+ content.push(cursor + displayLabel + " " + style.dim("–") + " " + badge);
861
+ }
862
+ };
509
863
  if (hasOnlyExecute && state.formSearchQuery) {
510
864
  content.push(" " + style.dim("No matching parameters"));
511
865
  content.push("");
512
866
  }
513
867
  else {
514
- // Scrolling: items before the Execute sentinel
515
- const paramItems = filtered.filter((idx) => idx !== -1);
516
- const visStart = state.formScrollTop;
517
- const visEnd = Math.min(paramItems.length, visStart + listHeight);
518
- const visible = paramItems.slice(visStart, visEnd);
519
- for (const fieldIdx of visible) {
520
- const field = fields[fieldIdx];
521
- const nameLabel = field.name + (field.required ? " *" : "");
522
- const paddedName = nameLabel.padEnd(maxLabelWidth);
523
- const val = state.formValues[field.name] || "";
524
- const valStr = formFieldValueDisplay(val, valueAvail);
525
- const listPos = filtered.indexOf(fieldIdx);
526
- const selected = listPos === state.formListCursor;
527
- const prefix = selected ? " ❯ " : " ";
528
- if (selected) {
529
- content.push(style.boldYellow(prefix + paddedName) + " " + valStr);
868
+ // Required fields (always visible)
869
+ for (const fieldIdx of requiredIdxs) {
870
+ renderField(fieldIdx);
871
+ }
872
+ // Optional fields separator
873
+ if (hasOptional) {
874
+ if (showingOptional) {
875
+ if (requiredIdxs.length > 0)
876
+ content.push("");
877
+ content.push(" " + style.dim("── optional ──"));
878
+ const visibleOptional = optionalIdxs.slice(0, listHeight - requiredIdxs.length - 2);
879
+ for (const fieldIdx of visibleOptional) {
880
+ renderField(fieldIdx);
881
+ }
530
882
  }
531
883
  else {
532
- content.push(prefix + paddedName + " " + valStr);
884
+ content.push("");
885
+ const optLabel = filledOptionalCount > 0
886
+ ? `── ${optionalIdxs.length} optional (${filledOptionalCount} set) · 'o' to show ──`
887
+ : `── ${optionalIdxs.length} optional · 'o' to show ──`;
888
+ content.push(" " + style.dim(optLabel));
533
889
  }
534
890
  }
535
891
  }
@@ -556,22 +912,34 @@ function renderFormPaletteMode(state, title, fields, contentHeight, innerWidth)
556
912
  content.push(" " + style.red("Required: " + names));
557
913
  }
558
914
  }
559
- // Description of highlighted field
915
+ // Description of highlighted field or Execute hint
560
916
  const highlightedIdx = filtered[state.formListCursor];
561
917
  if (highlightedIdx !== undefined && highlightedIdx >= 0 && highlightedIdx < fields.length) {
562
- const desc = fields[highlightedIdx].prop.description;
563
- if (desc) {
918
+ const prop = fields[highlightedIdx].prop;
919
+ if (prop.description) {
564
920
  content.push("");
565
- const wrapped = wrapText(desc, innerWidth - 4);
921
+ const wrapped = wrapText(prop.description, innerWidth - 4);
566
922
  for (const line of wrapped) {
567
923
  content.push(" " + style.dim(line));
568
924
  }
569
925
  }
926
+ if (prop.examples?.length) {
927
+ const exStr = prop.examples.map((e) => typeof e === "string" ? e : JSON.stringify(e)).join(", ");
928
+ content.push(" " + style.dim("e.g. ") + style.dim(style.cyan(truncateVisible(exStr, innerWidth - 10))));
929
+ }
570
930
  }
931
+ else if (highlightedIdx === -1) {
932
+ content.push("");
933
+ content.push(" " + style.dim("Press enter to run"));
934
+ }
935
+ // Footer hints
936
+ const hasUnfilledRequired = requiredFields.some((f) => !state.formValues[f.name]?.trim());
937
+ const tabHint = hasUnfilledRequired ? " · tab next required" : "";
938
+ const optionalHint = hasOptional ? " · o optional" : "";
571
939
  return renderLayout({
572
940
  breadcrumb: style.boldYellow("Readwise") + style.dim(" › ") + style.bold(title),
573
941
  content,
574
- footer: style.dim("type to filter ↑↓ navigate enter edit/run esc back"),
942
+ footer: style.dim("↑↓ navigate · enter edit" + tabHint + optionalHint + " · esc back"),
575
943
  });
576
944
  }
577
945
  function renderFormEditMode(state, title, fields, _contentHeight, innerWidth) {
@@ -579,10 +947,22 @@ function renderFormEditMode(state, title, fields, _contentHeight, innerWidth) {
579
947
  const content = [];
580
948
  content.push("");
581
949
  content.push(" " + style.bold(title));
950
+ // Show tool description for context
951
+ const toolDesc = state.formStack.length > 0
952
+ ? state.formStack[state.formStack.length - 1].parentFields
953
+ .find((f) => f.name === state.formStack[state.formStack.length - 1].parentFieldName)
954
+ ?.prop.items?.description
955
+ : state.selectedTool.description;
956
+ if (toolDesc) {
957
+ const wrapped = wrapText(toolDesc, innerWidth - 4);
958
+ for (const line of wrapped) {
959
+ content.push(" " + style.dim(line));
960
+ }
961
+ }
582
962
  content.push("");
583
- // Field name
963
+ // Field label
584
964
  const nameLabel = field.name + (field.required ? " *" : "");
585
- content.push(" " + style.boldYellow("❯ " + nameLabel));
965
+ content.push(" " + style.bold(nameLabel));
586
966
  // Field description
587
967
  if (field.prop.description) {
588
968
  const wrapped = wrapText(field.prop.description, innerWidth - 4);
@@ -590,15 +970,15 @@ function renderFormEditMode(state, title, fields, _contentHeight, innerWidth) {
590
970
  content.push(" " + style.dim(line));
591
971
  }
592
972
  }
973
+ if (field.prop.examples?.length) {
974
+ const exStr = field.prop.examples.map((e) => typeof e === "string" ? e : JSON.stringify(e)).join(", ");
975
+ content.push(" " + style.dim("e.g. ") + style.dim(style.cyan(truncateVisible(exStr, innerWidth - 10))));
976
+ }
593
977
  content.push("");
594
978
  // Editor area
979
+ const kind = classifyField(field.prop);
595
980
  const eVals = field.prop.enum || field.prop.items?.enum;
596
- const isArrayObj = isArrayOfObjects(field.prop);
597
- const isArrayEnum = !isArrayObj && field.prop.type === "array" && !!field.prop.items?.enum;
598
- const isArrayText = !isArrayObj && field.prop.type === "array" && !field.prop.items?.enum;
599
- const isBool = field.prop.type === "boolean";
600
- const dateFmt = dateFieldFormat(field.prop);
601
- if (isArrayObj) {
981
+ if (kind === "arrayObj") {
602
982
  // Array-of-objects editor: show existing items + "Add new item"
603
983
  const existing = state.formValues[field.name] || "[]";
604
984
  let items = [];
@@ -628,10 +1008,11 @@ function renderFormEditMode(state, title, fields, _contentHeight, innerWidth) {
628
1008
  content.push(" " + style.dim("+") + " Add new item");
629
1009
  }
630
1010
  }
631
- else if (dateFmt) {
1011
+ else if (kind === "date") {
1012
+ const dateFmt = dateFieldFormat(field.prop);
632
1013
  content.push(" " + renderDateParts(state.dateParts, state.datePartCursor, dateFmt));
633
1014
  }
634
- else if (isArrayEnum && eVals) {
1015
+ else if (kind === "arrayEnum" && eVals) {
635
1016
  // Multi-select picker
636
1017
  for (let ci = 0; ci < eVals.length; ci++) {
637
1018
  const isCursor = ci === state.formEnumCursor;
@@ -642,7 +1023,7 @@ function renderFormEditMode(state, title, fields, _contentHeight, innerWidth) {
642
1023
  content.push(isCursor ? style.bold(label) : label);
643
1024
  }
644
1025
  }
645
- else if (isArrayText) {
1026
+ else if (kind === "arrayText") {
646
1027
  // Tag-style list editor: navigable items + text input at bottom
647
1028
  const existing = state.formValues[field.name] || "";
648
1029
  const items = existing ? existing.split(",").map((s) => s.trim()).filter(Boolean) : [];
@@ -668,8 +1049,8 @@ function renderFormEditMode(state, title, fields, _contentHeight, innerWidth) {
668
1049
  content.push(" " + style.dim("bksp ") + style.dim("remove item"));
669
1050
  }
670
1051
  }
671
- else if (eVals || isBool) {
672
- const choices = isBool ? ["true", "false"] : eVals;
1052
+ else if (kind === "enum" || kind === "bool") {
1053
+ const choices = kind === "bool" ? ["true", "false"] : eVals;
673
1054
  for (let ci = 0; ci < choices.length; ci++) {
674
1055
  const sel = ci === state.formEnumCursor;
675
1056
  const choiceLine = (sel ? " › " : " ") + choices[ci];
@@ -677,41 +1058,73 @@ function renderFormEditMode(state, title, fields, _contentHeight, innerWidth) {
677
1058
  }
678
1059
  }
679
1060
  else {
680
- // Text editor
681
- const lines = state.formInputBuf.split("\n");
682
- for (let li = 0; li < lines.length; li++) {
683
- const prefix = li === 0 ? " " + style.yellow("❯") + " " : " ";
684
- if (li === lines.length - 1) {
685
- content.push(prefix + style.cyan(lines[li]) + style.inverse(" "));
1061
+ // Text editor with cursor position
1062
+ const prefix0 = " ";
1063
+ if (!state.formInputBuf) {
1064
+ // Show placeholder text when input is empty
1065
+ let placeholder = "type a value";
1066
+ if (field.prop.examples?.length) {
1067
+ placeholder = String(field.prop.examples[0]);
686
1068
  }
687
- else {
688
- content.push(prefix + style.cyan(lines[li]));
1069
+ else if (field.prop.description) {
1070
+ placeholder = field.prop.description.toLowerCase().replace(/[.!]$/, "");
1071
+ }
1072
+ else if (field.prop.type === "integer" || field.prop.type === "number") {
1073
+ placeholder = "enter a number";
1074
+ }
1075
+ content.push(prefix0 + style.inverse(" ") + style.dim(" " + placeholder + "…"));
1076
+ }
1077
+ else {
1078
+ const lines = state.formInputBuf.split("\n");
1079
+ // Find cursor line and column from flat position
1080
+ let cursorLine = 0;
1081
+ let cursorCol = state.formInputCursorPos;
1082
+ for (let li = 0; li < lines.length; li++) {
1083
+ if (cursorCol <= lines[li].length) {
1084
+ cursorLine = li;
1085
+ break;
1086
+ }
1087
+ cursorCol -= lines[li].length + 1;
1088
+ }
1089
+ for (let li = 0; li < lines.length; li++) {
1090
+ const prefix = li === 0 ? prefix0 : " ";
1091
+ const lineText = lines[li];
1092
+ if (li === cursorLine) {
1093
+ const before = lineText.slice(0, cursorCol);
1094
+ const cursorChar = cursorCol < lineText.length ? lineText[cursorCol] : " ";
1095
+ const after = cursorCol < lineText.length ? lineText.slice(cursorCol + 1) : "";
1096
+ content.push(prefix + style.cyan(before) + style.inverse(cursorChar) + style.cyan(after));
1097
+ }
1098
+ else {
1099
+ content.push(prefix + style.cyan(lineText));
1100
+ }
689
1101
  }
690
1102
  }
691
1103
  }
692
- let footer;
693
- if (isArrayObj) {
694
- footer = style.dim("↑↓ navigate enter add/select backspace delete esc back");
695
- }
696
- else if (dateFmt) {
697
- footer = style.dim("←→ part ↑↓ adjust t today enter confirm esc cancel");
698
- }
699
- else if (isArrayEnum) {
700
- footer = style.dim("space toggle enter select esc confirm");
701
- }
702
- else if (isArrayText) {
703
- footer = style.dim("↑↓ navigate enter add/edit backspace delete esc confirm");
704
- }
705
- else if (eVals || isBool) {
706
- footer = style.dim("↑↓ navigate enter confirm esc cancel");
707
- }
708
- else {
709
- footer = style.dim("enter confirm shift+enter newline esc cancel");
1104
+ // Show remaining required fields hint (for text editors)
1105
+ if (kind === "text") {
1106
+ const requiredFields = fields.filter((f) => f.required);
1107
+ const filledCount = requiredFields.filter((f) => {
1108
+ if (f.name === field.name)
1109
+ return !!state.formInputBuf.trim(); // current field
1110
+ return !!state.formValues[f.name]?.trim();
1111
+ }).length;
1112
+ const remaining = requiredFields.length - filledCount;
1113
+ content.push("");
1114
+ if (remaining <= 0) {
1115
+ content.push(" " + style.dim("Then press enter to confirm → Execute"));
1116
+ }
1117
+ else if (remaining === 1 && !state.formInputBuf.trim()) {
1118
+ content.push(" " + style.dim("Type a value, then press enter"));
1119
+ }
1120
+ else {
1121
+ content.push(" " + style.dim(`${remaining} required field${remaining > 1 ? "s" : ""} remaining`));
1122
+ }
710
1123
  }
711
1124
  return renderLayout({
712
1125
  breadcrumb: style.boldYellow("Readwise") + style.dim(" › ") + style.bold(title),
713
1126
  content,
714
- footer,
1127
+ footer: style.dim(footerForFieldKind(kind)),
715
1128
  });
716
1129
  }
717
1130
  function renderLoading(state) {
@@ -777,7 +1190,7 @@ function renderResults(state) {
777
1190
  content,
778
1191
  footer: state.quitConfirm
779
1192
  ? style.yellow("Press q again to quit")
780
- : style.dim("enter/esc back q quit"),
1193
+ : style.dim("enter/esc back · q quit"),
781
1194
  });
782
1195
  }
783
1196
  // Success screen for empty results
@@ -797,7 +1210,7 @@ function renderResults(state) {
797
1210
  content,
798
1211
  footer: state.quitConfirm
799
1212
  ? style.yellow("Press q again to quit")
800
- : style.dim("enter/esc back q quit"),
1213
+ : style.dim("enter/esc back · q quit"),
801
1214
  });
802
1215
  }
803
1216
  const rawContent = state.error || state.result;
@@ -825,7 +1238,7 @@ function renderResults(state) {
825
1238
  content,
826
1239
  footer: state.quitConfirm
827
1240
  ? style.yellow("Press q again to quit")
828
- : style.dim(scrollHint + "↑↓←→ scroll esc back q quit"),
1241
+ : style.dim(scrollHint + "↑↓←→ scroll · esc back · q quit"),
829
1242
  });
830
1243
  }
831
1244
  function renderState(state) {
@@ -852,10 +1265,8 @@ function handleCommandListInput(state, key) {
852
1265
  // search input uses: LOGO.length + 1 (blank) + 1 (search line) + 1 (blank)
853
1266
  const logoUsed = LOGO.length + 3;
854
1267
  const listHeight = Math.max(1, contentHeight - logoUsed);
855
- if (key.ctrl && key.name === "c")
856
- return "exit";
857
- // Escape: clear query if non-empty, otherwise quit confirm
858
- if (key.name === "escape") {
1268
+ // Escape / ctrl+c / q: clear query if non-empty, otherwise quit confirm
1269
+ if (key.name === "escape" || (key.ctrl && key.name === "c")) {
859
1270
  if (state.searchQuery) {
860
1271
  const filtered = filterCommands(state.tools, "");
861
1272
  const sel = selectableIndices(filtered);
@@ -937,12 +1348,7 @@ function handleCommandListInput(state, key) {
937
1348
  const nameColWidth = Math.max(...fields.map((f) => f.name.length + (f.required ? 2 : 0)), 6) + 1;
938
1349
  const formValues = {};
939
1350
  for (const f of fields) {
940
- if (f.prop.default != null) {
941
- formValues[f.name] = String(f.prop.default);
942
- }
943
- else {
944
- formValues[f.name] = "";
945
- }
1351
+ formValues[f.name] = "";
946
1352
  }
947
1353
  if (fields.length === 0) {
948
1354
  return {
@@ -960,13 +1366,16 @@ function handleCommandListInput(state, key) {
960
1366
  formEditFieldIdx: -1,
961
1367
  formEditing: false,
962
1368
  formInputBuf: "",
1369
+ formInputCursorPos: 0,
963
1370
  formEnumCursor: 0,
964
1371
  formEnumSelected: new Set(),
965
- formShowRequired: false,
1372
+ formShowRequired: false, formShowOptional: false,
966
1373
  formStack: [],
967
1374
  };
968
1375
  }
969
- return {
1376
+ const filteredIndices = filterFormFields(fields, "");
1377
+ const firstBlankRequired = fields.findIndex((f) => f.required && !formValues[f.name]?.trim());
1378
+ const baseState = {
970
1379
  ...s,
971
1380
  view: "form",
972
1381
  selectedTool: tool,
@@ -975,17 +1384,23 @@ function handleCommandListInput(state, key) {
975
1384
  formValues,
976
1385
  formSearchQuery: "",
977
1386
  formSearchCursorPos: 0,
978
- formFilteredIndices: filterFormFields(fields, ""),
979
- formListCursor: defaultFormCursor(fields, filterFormFields(fields, ""), formValues),
1387
+ formFilteredIndices: filteredIndices,
1388
+ formListCursor: defaultFormCursor(fields, filteredIndices, formValues),
980
1389
  formScrollTop: 0,
981
1390
  formEditFieldIdx: -1,
982
1391
  formEditing: false,
983
1392
  formInputBuf: "",
1393
+ formInputCursorPos: 0,
984
1394
  formEnumCursor: 0,
985
1395
  formEnumSelected: new Set(),
986
- formShowRequired: false,
1396
+ formShowRequired: false, formShowOptional: false,
987
1397
  formStack: [],
988
1398
  };
1399
+ // Auto-open first required field
1400
+ if (firstBlankRequired >= 0) {
1401
+ return startEditingField(baseState, firstBlankRequired);
1402
+ }
1403
+ return baseState;
989
1404
  }
990
1405
  }
991
1406
  return s;
@@ -1000,19 +1415,158 @@ function handleCommandListInput(state, key) {
1000
1415
  }
1001
1416
  return s;
1002
1417
  }
1003
- // Printable characters: insert into search query
1004
- if (!key.ctrl && key.raw && key.raw.length === 1 && key.raw >= " ") {
1005
- const newQuery = s.searchQuery.slice(0, s.searchCursorPos) + key.raw + s.searchQuery.slice(s.searchCursorPos);
1006
- const filtered = filterCommands(s.tools, newQuery);
1007
- const sel = selectableIndices(filtered);
1008
- return { ...s, searchQuery: newQuery, searchCursorPos: s.searchCursorPos + 1, filteredItems: filtered, listCursor: sel[0] ?? 0, listScrollTop: 0 };
1418
+ // Printable characters or paste: insert into search query
1419
+ if (key.name === "paste" || (!key.ctrl && key.raw && key.raw.length === 1 && key.raw >= " ")) {
1420
+ const text = (key.name === "paste" ? key.raw.replace(/[\x00-\x1f\x7f]/g, "") : key.raw) || "";
1421
+ if (text) {
1422
+ const newQuery = s.searchQuery.slice(0, s.searchCursorPos) + text + s.searchQuery.slice(s.searchCursorPos);
1423
+ const filtered = filterCommands(s.tools, newQuery);
1424
+ const sel = selectableIndices(filtered);
1425
+ return { ...s, searchQuery: newQuery, searchCursorPos: s.searchCursorPos + text.length, filteredItems: filtered, listCursor: sel[0] ?? 0, listScrollTop: 0 };
1426
+ }
1009
1427
  }
1010
1428
  return s;
1011
1429
  }
1430
+ function startEditingField(state, fieldIdx) {
1431
+ const field = state.fields[fieldIdx];
1432
+ if (isArrayOfObjects(field.prop)) {
1433
+ const existing = state.formValues[field.name] || "[]";
1434
+ let items = [];
1435
+ try {
1436
+ items = JSON.parse(existing);
1437
+ }
1438
+ catch { /* */ }
1439
+ return { ...state, formEditing: true, formEditFieldIdx: fieldIdx, formEnumCursor: items.length };
1440
+ }
1441
+ const dateFmt = dateFieldFormat(field.prop);
1442
+ if (dateFmt) {
1443
+ const existing = state.formValues[field.name] || "";
1444
+ const parts = parseDateParts(existing, dateFmt) || todayParts(dateFmt);
1445
+ return { ...state, formEditing: true, formEditFieldIdx: fieldIdx, dateParts: parts, datePartCursor: 0 };
1446
+ }
1447
+ const enumValues = field.prop.enum || field.prop.items?.enum;
1448
+ const isBool = field.prop.type === "boolean";
1449
+ const isArrayEnum = !isArrayOfObjects(field.prop) && field.prop.type === "array" && !!field.prop.items?.enum;
1450
+ if (isArrayEnum && enumValues) {
1451
+ const curVal = state.formValues[field.name] || "";
1452
+ const selected = new Set();
1453
+ if (curVal) {
1454
+ const parts = curVal.split(",").map((s) => s.trim());
1455
+ for (const p of parts) {
1456
+ const idx = enumValues.indexOf(p);
1457
+ if (idx >= 0)
1458
+ selected.add(idx);
1459
+ }
1460
+ }
1461
+ return { ...state, formEditing: true, formEditFieldIdx: fieldIdx, formEnumCursor: 0, formEnumSelected: selected };
1462
+ }
1463
+ if (enumValues || isBool) {
1464
+ const choices = isBool ? ["true", "false"] : enumValues;
1465
+ const curVal = state.formValues[field.name] || "";
1466
+ const idx = choices.indexOf(curVal);
1467
+ return { ...state, formEditing: true, formEditFieldIdx: fieldIdx, formEnumCursor: idx >= 0 ? idx : 0 };
1468
+ }
1469
+ if (field.prop.type === "array" && !field.prop.items?.enum) {
1470
+ const existing = state.formValues[field.name] || "";
1471
+ const itemCount = existing ? existing.split(",").map((s) => s.trim()).filter(Boolean).length : 0;
1472
+ return { ...state, formEditing: true, formEditFieldIdx: fieldIdx, formInputBuf: "", formInputCursorPos: 0, formEnumCursor: itemCount };
1473
+ }
1474
+ const editBuf = state.formValues[field.name] || "";
1475
+ return { ...state, formEditing: true, formEditFieldIdx: fieldIdx, formInputBuf: editBuf, formInputCursorPos: editBuf.length };
1476
+ }
1012
1477
  function handleFormInput(state, key) {
1013
- if (state.formEditing)
1014
- return handleFormEditInput(state, key);
1015
- return handleFormPaletteInput(state, key);
1478
+ // Sub-forms use existing palette/edit handlers
1479
+ if (state.formStack.length > 0) {
1480
+ if (state.formEditing)
1481
+ return handleFormEditInput(state, key);
1482
+ return handleFormPaletteInput(state, key);
1483
+ }
1484
+ // Command builder: editing a field
1485
+ if (state.formEditing) {
1486
+ const result = handleFormEditInput(state, key);
1487
+ if (result === "submit")
1488
+ return result;
1489
+ // Auto-advance: if editing just ended via confirm (not cancel), jump to next blank required field
1490
+ if (!result.formEditing && state.formEditing) {
1491
+ const wasCancel = key.name === "escape";
1492
+ if (!wasCancel) {
1493
+ const nextBlank = result.fields.findIndex((f) => f.required && !result.formValues[f.name]?.trim());
1494
+ if (nextBlank >= 0) {
1495
+ return startEditingField(result, nextBlank);
1496
+ }
1497
+ }
1498
+ }
1499
+ return result;
1500
+ }
1501
+ // Command builder: optional picker
1502
+ if (state.formShowOptional) {
1503
+ return handleOptionalPickerInput(state, key);
1504
+ }
1505
+ // Command builder: ready state
1506
+ return handleCommandBuilderReadyInput(state, key);
1507
+ }
1508
+ function commandListReset(tools) {
1509
+ const filteredItems = buildCommandList(tools);
1510
+ const sel = selectableIndices(filteredItems);
1511
+ return {
1512
+ view: "commands", selectedTool: null,
1513
+ searchQuery: "", searchCursorPos: 0,
1514
+ filteredItems, listCursor: sel[0] ?? 0, listScrollTop: 0,
1515
+ };
1516
+ }
1517
+ function handleCommandBuilderReadyInput(state, key) {
1518
+ if (key.name === "escape" || (key.ctrl && key.name === "c")) {
1519
+ return { ...state, ...commandListReset(state.tools) };
1520
+ }
1521
+ if (key.name === "return") {
1522
+ if (missingRequiredFields(state.fields, state.formValues).length === 0) {
1523
+ return "submit";
1524
+ }
1525
+ // Jump to first missing required field
1526
+ const nextBlank = state.fields.findIndex((f) => f.required && !state.formValues[f.name]?.trim());
1527
+ if (nextBlank >= 0) {
1528
+ return startEditingField(state, nextBlank);
1529
+ }
1530
+ return state;
1531
+ }
1532
+ if (key.name === "tab") {
1533
+ const hasOptional = state.fields.some((f) => !f.required);
1534
+ if (hasOptional) {
1535
+ return { ...state, formShowOptional: true, formListCursor: 0 };
1536
+ }
1537
+ return state;
1538
+ }
1539
+ // Backspace: re-edit last set field
1540
+ if (key.name === "backspace") {
1541
+ for (let i = state.fields.length - 1; i >= 0; i--) {
1542
+ if (state.formValues[state.fields[i].name]?.trim()) {
1543
+ return startEditingField(state, i);
1544
+ }
1545
+ }
1546
+ return state;
1547
+ }
1548
+ return state;
1549
+ }
1550
+ function handleOptionalPickerInput(state, key) {
1551
+ const optionalFields = state.fields
1552
+ .map((f, i) => ({ field: f, idx: i }))
1553
+ .filter(({ field }) => !field.required);
1554
+ if (key.name === "escape" || (key.ctrl && key.name === "c")) {
1555
+ return { ...state, formShowOptional: false };
1556
+ }
1557
+ if (key.name === "up") {
1558
+ return { ...state, formListCursor: state.formListCursor > 0 ? state.formListCursor - 1 : optionalFields.length - 1 };
1559
+ }
1560
+ if (key.name === "down") {
1561
+ return { ...state, formListCursor: state.formListCursor < optionalFields.length - 1 ? state.formListCursor + 1 : 0 };
1562
+ }
1563
+ if (key.name === "return") {
1564
+ const selected = optionalFields[state.formListCursor];
1565
+ if (selected) {
1566
+ return startEditingField({ ...state, formShowOptional: false }, selected.idx);
1567
+ }
1568
+ }
1569
+ return state;
1016
1570
  }
1017
1571
  function handleFormPaletteInput(state, key) {
1018
1572
  const { fields, formFilteredIndices: filtered, formListCursor, formSearchQuery } = state;
@@ -1055,13 +1609,71 @@ function handleFormPaletteInput(state, key) {
1055
1609
  formFilteredIndices: parentFiltered,
1056
1610
  formListCursor: defaultFormCursor(entry.parentFields, parentFiltered, entry.parentValues),
1057
1611
  formScrollTop: 0,
1058
- formShowRequired: false,
1612
+ formShowRequired: false, formShowOptional: false,
1059
1613
  formInputBuf: "",
1614
+ formInputCursorPos: 0,
1060
1615
  };
1061
1616
  }
1062
- const resetFiltered = buildCommandList(state.tools);
1063
- const resetSel = selectableIndices(resetFiltered);
1064
- return { ...state, view: "commands", selectedTool: null, searchQuery: "", searchCursorPos: 0, filteredItems: resetFiltered, listCursor: resetSel[0] ?? 0, listScrollTop: 0 };
1617
+ return { ...state, ...commandListReset(state.tools) };
1618
+ }
1619
+ // Tab: jump to next unfilled required field
1620
+ if (key.name === "tab") {
1621
+ const unfilledRequired = filtered
1622
+ .map((idx, listPos) => ({ idx, listPos }))
1623
+ .filter(({ idx }) => {
1624
+ if (idx < 0 || idx >= fields.length)
1625
+ return false;
1626
+ const f = fields[idx];
1627
+ if (!f.required)
1628
+ return false;
1629
+ const val = state.formValues[f.name]?.trim();
1630
+ if (!val)
1631
+ return true;
1632
+ if (isArrayOfObjects(f.prop)) {
1633
+ try {
1634
+ return JSON.parse(val).length === 0;
1635
+ }
1636
+ catch {
1637
+ return true;
1638
+ }
1639
+ }
1640
+ return false;
1641
+ });
1642
+ if (unfilledRequired.length > 0) {
1643
+ // Find the next one after current cursor, wrapping around
1644
+ const after = unfilledRequired.find((u) => u.listPos > formListCursor);
1645
+ const target = after || unfilledRequired[0];
1646
+ let scroll = state.formScrollTop;
1647
+ const paramItems = filtered.filter((idx) => idx !== -1);
1648
+ if (target.idx >= 0) {
1649
+ const posInParams = paramItems.indexOf(target.idx);
1650
+ if (posInParams < scroll)
1651
+ scroll = posInParams;
1652
+ if (posInParams >= scroll + listHeight)
1653
+ scroll = posInParams - listHeight + 1;
1654
+ }
1655
+ return { ...state, formListCursor: target.listPos, formScrollTop: scroll };
1656
+ }
1657
+ // No unfilled required fields — jump to Execute
1658
+ const execPos = filtered.indexOf(-1);
1659
+ if (execPos >= 0)
1660
+ return { ...state, formListCursor: execPos };
1661
+ return state;
1662
+ }
1663
+ // 'o' key: toggle optional fields visibility (when not searching)
1664
+ if (key.raw === "o" && !key.ctrl && !formSearchQuery) {
1665
+ const optionalExists = filtered.some((idx) => idx >= 0 && idx < fields.length && !fields[idx].required);
1666
+ if (optionalExists) {
1667
+ const newShow = !state.formShowOptional;
1668
+ if (!newShow) {
1669
+ const curIdx = filtered[formListCursor];
1670
+ if (curIdx !== undefined && curIdx >= 0 && curIdx < fields.length && !fields[curIdx].required) {
1671
+ const execPos = filtered.indexOf(-1);
1672
+ return { ...state, formShowOptional: false, formListCursor: execPos >= 0 ? execPos : 0 };
1673
+ }
1674
+ }
1675
+ return { ...state, formShowOptional: newShow };
1676
+ }
1065
1677
  }
1066
1678
  // Arrow left/right: move text cursor within search input
1067
1679
  if (key.name === "left") {
@@ -1070,9 +1682,25 @@ function handleFormPaletteInput(state, key) {
1070
1682
  if (key.name === "right") {
1071
1683
  return { ...state, formSearchCursorPos: Math.min(formSearchQuery.length, state.formSearchCursorPos + 1) };
1072
1684
  }
1073
- // Arrow up/down: navigate filtered list (cycling)
1685
+ // Helper: check if a position in filtered is navigable (skip collapsed optional fields)
1686
+ const isNavigable = (listPos) => {
1687
+ const idx = filtered[listPos];
1688
+ if (idx === undefined)
1689
+ return false;
1690
+ if (idx === -1)
1691
+ return true; // Execute always navigable
1692
+ if (!state.formShowOptional && !state.formSearchQuery && idx >= 0 && idx < fields.length && !fields[idx].required)
1693
+ return false;
1694
+ return true;
1695
+ };
1696
+ // Arrow up/down: navigate filtered list (cycling, skipping hidden items)
1074
1697
  if (key.name === "up") {
1075
- const next = formListCursor > 0 ? formListCursor - 1 : filtered.length - 1;
1698
+ let next = formListCursor;
1699
+ for (let i = 0; i < filtered.length; i++) {
1700
+ next = next > 0 ? next - 1 : filtered.length - 1;
1701
+ if (isNavigable(next))
1702
+ break;
1703
+ }
1076
1704
  let scroll = state.formScrollTop;
1077
1705
  const itemIdx = filtered[next];
1078
1706
  if (itemIdx !== -1) {
@@ -1080,14 +1708,18 @@ function handleFormPaletteInput(state, key) {
1080
1708
  const posInParams = paramItems.indexOf(itemIdx);
1081
1709
  if (posInParams < scroll)
1082
1710
  scroll = posInParams;
1083
- // Wrap to bottom: reset scroll to show end of list
1084
1711
  if (next > formListCursor)
1085
1712
  scroll = Math.max(0, paramItems.length - listHeight);
1086
1713
  }
1087
1714
  return { ...state, formListCursor: next, formScrollTop: scroll };
1088
1715
  }
1089
1716
  if (key.name === "down") {
1090
- const next = formListCursor < filtered.length - 1 ? formListCursor + 1 : 0;
1717
+ let next = formListCursor;
1718
+ for (let i = 0; i < filtered.length; i++) {
1719
+ next = next < filtered.length - 1 ? next + 1 : 0;
1720
+ if (isNavigable(next))
1721
+ break;
1722
+ }
1091
1723
  let scroll = state.formScrollTop;
1092
1724
  const itemIdx = filtered[next];
1093
1725
  if (itemIdx !== -1) {
@@ -1095,7 +1727,6 @@ function handleFormPaletteInput(state, key) {
1095
1727
  const posInParams = paramItems.indexOf(itemIdx);
1096
1728
  if (posInParams >= scroll + listHeight)
1097
1729
  scroll = posInParams - listHeight + 1;
1098
- // Wrap to top: reset scroll
1099
1730
  if (next < formListCursor)
1100
1731
  scroll = 0;
1101
1732
  }
@@ -1119,51 +1750,7 @@ function handleFormPaletteInput(state, key) {
1119
1750
  return { ...state, formShowRequired: true };
1120
1751
  }
1121
1752
  if (highlightedIdx !== undefined && highlightedIdx >= 0 && highlightedIdx < fields.length) {
1122
- const field = fields[highlightedIdx];
1123
- // Array-of-objects: enter edit mode with cursor on "Add new item"
1124
- if (isArrayOfObjects(field.prop)) {
1125
- const existing = state.formValues[field.name] || "[]";
1126
- let items = [];
1127
- try {
1128
- items = JSON.parse(existing);
1129
- }
1130
- catch { /* */ }
1131
- return { ...state, formEditing: true, formEditFieldIdx: highlightedIdx, formEnumCursor: items.length };
1132
- }
1133
- const dateFmt = dateFieldFormat(field.prop);
1134
- const enumValues = field.prop.enum || field.prop.items?.enum;
1135
- const isBool = field.prop.type === "boolean";
1136
- if (dateFmt) {
1137
- const existing = state.formValues[field.name] || "";
1138
- const parts = parseDateParts(existing, dateFmt) || todayParts(dateFmt);
1139
- return { ...state, formEditing: true, formEditFieldIdx: highlightedIdx, dateParts: parts, datePartCursor: 0 };
1140
- }
1141
- const isArrayEnum = !isArrayOfObjects(field.prop) && field.prop.type === "array" && !!field.prop.items?.enum;
1142
- if (isArrayEnum && enumValues) {
1143
- const curVal = state.formValues[field.name] || "";
1144
- const selected = new Set();
1145
- if (curVal) {
1146
- const parts = curVal.split(",").map((s) => s.trim());
1147
- for (const p of parts) {
1148
- const idx = enumValues.indexOf(p);
1149
- if (idx >= 0)
1150
- selected.add(idx);
1151
- }
1152
- }
1153
- return { ...state, formEditing: true, formEditFieldIdx: highlightedIdx, formEnumCursor: 0, formEnumSelected: selected };
1154
- }
1155
- if (enumValues || isBool) {
1156
- const choices = isBool ? ["true", "false"] : enumValues;
1157
- const curVal = state.formValues[field.name] || "";
1158
- const idx = choices.indexOf(curVal);
1159
- return { ...state, formEditing: true, formEditFieldIdx: highlightedIdx, formEnumCursor: idx >= 0 ? idx : 0 };
1160
- }
1161
- if (field.prop.type === "array" && !field.prop.items?.enum) {
1162
- const existing = state.formValues[field.name] || "";
1163
- const itemCount = existing ? existing.split(",").map((s) => s.trim()).filter(Boolean).length : 0;
1164
- return { ...state, formEditing: true, formEditFieldIdx: highlightedIdx, formInputBuf: "", formEnumCursor: itemCount };
1165
- }
1166
- return { ...state, formEditing: true, formEditFieldIdx: highlightedIdx, formInputBuf: state.formValues[field.name] || "" };
1753
+ return startEditingField(state, highlightedIdx);
1167
1754
  }
1168
1755
  return state;
1169
1756
  }
@@ -1176,11 +1763,14 @@ function handleFormPaletteInput(state, key) {
1176
1763
  }
1177
1764
  return state;
1178
1765
  }
1179
- // Printable characters: insert into search query
1180
- if (!key.ctrl && key.raw && key.raw.length === 1 && key.raw >= " ") {
1181
- const newQuery = formSearchQuery.slice(0, state.formSearchCursorPos) + key.raw + formSearchQuery.slice(state.formSearchCursorPos);
1182
- const newFiltered = filterFormFields(fields, newQuery);
1183
- return { ...state, formSearchQuery: newQuery, formSearchCursorPos: state.formSearchCursorPos + 1, formFilteredIndices: newFiltered, formListCursor: 0, formScrollTop: 0 };
1766
+ // Printable characters or paste: insert into search query
1767
+ if (key.name === "paste" || (!key.ctrl && key.raw && key.raw.length === 1 && key.raw >= " ")) {
1768
+ const text = (key.name === "paste" ? key.raw.replace(/[\x00-\x1f\x7f]/g, "") : key.raw) || "";
1769
+ if (text) {
1770
+ const newQuery = formSearchQuery.slice(0, state.formSearchCursorPos) + text + formSearchQuery.slice(state.formSearchCursorPos);
1771
+ const newFiltered = filterFormFields(fields, newQuery);
1772
+ return { ...state, formSearchQuery: newQuery, formSearchCursorPos: state.formSearchCursorPos + text.length, formFilteredIndices: newFiltered, formListCursor: 0, formScrollTop: 0 };
1773
+ }
1184
1774
  }
1185
1775
  return state;
1186
1776
  }
@@ -1192,7 +1782,7 @@ function handleFormEditInput(state, key) {
1192
1782
  const isBool = field.prop.type === "boolean";
1193
1783
  const resetPalette = (updatedValues) => {
1194
1784
  const f = filterFormFields(fields, "");
1195
- return { formSearchQuery: "", formSearchCursorPos: 0, formFilteredIndices: f, formListCursor: defaultFormCursor(fields, f, updatedValues ?? formValues), formScrollTop: 0, formShowRequired: false };
1785
+ return { formSearchQuery: "", formSearchCursorPos: 0, formFilteredIndices: f, formListCursor: defaultFormCursor(fields, f, updatedValues ?? formValues), formScrollTop: 0, formShowRequired: false, formShowOptional: false };
1196
1786
  };
1197
1787
  // Escape: cancel edit (for multi-select and tag editor, escape confirms since items are saved live)
1198
1788
  if (key.name === "escape") {
@@ -1204,7 +1794,7 @@ function handleFormEditInput(state, key) {
1204
1794
  const newValues = { ...formValues, [field.name]: val };
1205
1795
  return { ...state, formEditing: false, formEditFieldIdx: -1, formValues: newValues, formEnumSelected: new Set(), ...resetPalette(newValues) };
1206
1796
  }
1207
- return { ...state, formEditing: false, formEditFieldIdx: -1, formInputBuf: "", ...resetPalette() };
1797
+ return { ...state, formEditing: false, formEditFieldIdx: -1, formInputBuf: "", formInputCursorPos: 0, ...resetPalette() };
1208
1798
  }
1209
1799
  if (key.ctrl && key.name === "c")
1210
1800
  return "submit";
@@ -1280,10 +1870,11 @@ function handleFormEditInput(state, key) {
1280
1870
  formFilteredIndices: subFiltered,
1281
1871
  formListCursor: defaultFormCursor(subFields, subFiltered, subValues),
1282
1872
  formScrollTop: 0,
1283
- formShowRequired: false,
1873
+ formShowRequired: false, formShowOptional: false,
1284
1874
  formEnumCursor: 0,
1285
1875
  formEnumSelected: new Set(),
1286
1876
  formInputBuf: "",
1877
+ formInputCursorPos: 0,
1287
1878
  };
1288
1879
  }
1289
1880
  if (key.name === "backspace" && formEnumCursor < items.length) {
@@ -1391,7 +1982,7 @@ function handleFormEditInput(state, key) {
1391
1982
  const newItems = [...items];
1392
1983
  newItems.splice(formEnumCursor, 1);
1393
1984
  const newValues = { ...formValues, [field.name]: newItems.join(", ") };
1394
- return { ...state, formValues: newValues, formInputBuf: editVal, formEnumCursor: newItems.length };
1985
+ return { ...state, formValues: newValues, formInputBuf: editVal, formInputCursorPos: editVal.length, formEnumCursor: newItems.length };
1395
1986
  }
1396
1987
  if (key.name === "backspace") {
1397
1988
  // Delete item
@@ -1404,11 +1995,16 @@ function handleFormEditInput(state, key) {
1404
1995
  return state;
1405
1996
  }
1406
1997
  // Cursor on text input
1998
+ if (key.name === "paste") {
1999
+ // Paste: strip newlines for tag input
2000
+ const text = key.raw.replace(/\n/g, "");
2001
+ return { ...state, formInputBuf: formInputBuf + text, formInputCursorPos: formInputBuf.length + text.length };
2002
+ }
1407
2003
  if (key.name === "return") {
1408
2004
  if (formInputBuf.trim()) {
1409
2005
  items.push(formInputBuf.trim());
1410
2006
  const newValues = { ...formValues, [field.name]: items.join(", ") };
1411
- return { ...state, formValues: newValues, formInputBuf: "", formEnumCursor: items.length };
2007
+ return { ...state, formValues: newValues, formInputBuf: "", formInputCursorPos: 0, formEnumCursor: items.length };
1412
2008
  }
1413
2009
  // Empty input: confirm and close
1414
2010
  const newValues = { ...formValues, [field.name]: items.join(", ") };
@@ -1416,30 +2012,104 @@ function handleFormEditInput(state, key) {
1416
2012
  }
1417
2013
  if (key.name === "backspace") {
1418
2014
  if (formInputBuf) {
1419
- return { ...state, formInputBuf: formInputBuf.slice(0, -1) };
2015
+ return { ...state, formInputBuf: formInputBuf.slice(0, -1), formInputCursorPos: formInputBuf.length - 1 };
1420
2016
  }
1421
2017
  return state;
1422
2018
  }
1423
2019
  if (!key.ctrl && key.name !== "escape" && !key.raw.startsWith("\x1b")) {
1424
- return { ...state, formInputBuf: formInputBuf + key.raw };
2020
+ const clean = key.raw.replace(/[\x00-\x1f\x7f]/g, ""); // strip control chars for tags
2021
+ if (clean)
2022
+ return { ...state, formInputBuf: formInputBuf + clean, formInputCursorPos: formInputBuf.length + clean.length };
1425
2023
  }
1426
2024
  return state;
1427
2025
  }
1428
2026
  // Text editing mode
1429
- if (key.name === "return" && key.shift) {
1430
- // Shift+Enter: insert newline
1431
- return { ...state, formInputBuf: formInputBuf + "\n" };
2027
+ const pos = state.formInputCursorPos;
2028
+ if (key.name === "paste") {
2029
+ const newBuf = formInputBuf.slice(0, pos) + key.raw + formInputBuf.slice(pos);
2030
+ return { ...state, formInputBuf: newBuf, formInputCursorPos: pos + key.raw.length };
2031
+ }
2032
+ // Insert newline: Ctrl+J (\n), Shift+Enter, or Alt+Enter
2033
+ if (key.raw === "\n" || (key.name === "return" && key.shift)) {
2034
+ const newBuf = formInputBuf.slice(0, pos) + "\n" + formInputBuf.slice(pos);
2035
+ return { ...state, formInputBuf: newBuf, formInputCursorPos: pos + 1 };
1432
2036
  }
1433
2037
  if (key.name === "return") {
1434
- // Enter: confirm
2038
+ // Enter (\r): confirm value
1435
2039
  const newValues = { ...formValues, [field.name]: formInputBuf };
1436
- return { ...state, formEditing: false, formEditFieldIdx: -1, formValues: newValues, ...resetPalette(newValues) };
2040
+ return { ...state, formEditing: false, formEditFieldIdx: -1, formInputCursorPos: 0, formValues: newValues, ...resetPalette(newValues) };
2041
+ }
2042
+ if (key.name === "left") {
2043
+ return { ...state, formInputCursorPos: Math.max(0, pos - 1) };
2044
+ }
2045
+ if (key.name === "right") {
2046
+ return { ...state, formInputCursorPos: Math.min(formInputBuf.length, pos + 1) };
2047
+ }
2048
+ if (key.name === "wordLeft") {
2049
+ return { ...state, formInputCursorPos: prevWordBoundary(formInputBuf, pos) };
2050
+ }
2051
+ if (key.name === "wordRight") {
2052
+ return { ...state, formInputCursorPos: nextWordBoundary(formInputBuf, pos) };
2053
+ }
2054
+ if (key.name === "wordBackspace") {
2055
+ const boundary = prevWordBoundary(formInputBuf, pos);
2056
+ const newBuf = formInputBuf.slice(0, boundary) + formInputBuf.slice(pos);
2057
+ return { ...state, formInputBuf: newBuf, formInputCursorPos: boundary };
2058
+ }
2059
+ if (key.name === "up") {
2060
+ // Move cursor up one line
2061
+ const lines = formInputBuf.split("\n");
2062
+ let rem = pos;
2063
+ let line = 0;
2064
+ for (; line < lines.length; line++) {
2065
+ if (rem <= lines[line].length)
2066
+ break;
2067
+ rem -= lines[line].length + 1;
2068
+ }
2069
+ if (line > 0) {
2070
+ const col = Math.min(rem, lines[line - 1].length);
2071
+ let newPos = 0;
2072
+ for (let i = 0; i < line - 1; i++)
2073
+ newPos += lines[i].length + 1;
2074
+ newPos += col;
2075
+ return { ...state, formInputCursorPos: newPos };
2076
+ }
2077
+ return state;
2078
+ }
2079
+ if (key.name === "down") {
2080
+ // Move cursor down one line
2081
+ const lines = formInputBuf.split("\n");
2082
+ let rem = pos;
2083
+ let line = 0;
2084
+ for (; line < lines.length; line++) {
2085
+ if (rem <= lines[line].length)
2086
+ break;
2087
+ rem -= lines[line].length + 1;
2088
+ }
2089
+ if (line < lines.length - 1) {
2090
+ const col = Math.min(rem, lines[line + 1].length);
2091
+ let newPos = 0;
2092
+ for (let i = 0; i <= line; i++)
2093
+ newPos += lines[i].length + 1;
2094
+ newPos += col;
2095
+ return { ...state, formInputCursorPos: newPos };
2096
+ }
2097
+ return state;
1437
2098
  }
1438
2099
  if (key.name === "backspace") {
1439
- return { ...state, formInputBuf: formInputBuf.slice(0, -1) };
2100
+ if (pos > 0) {
2101
+ const newBuf = formInputBuf.slice(0, pos - 1) + formInputBuf.slice(pos);
2102
+ return { ...state, formInputBuf: newBuf, formInputCursorPos: pos - 1 };
2103
+ }
2104
+ return state;
1440
2105
  }
1441
2106
  if (!key.ctrl && key.name !== "escape" && !key.raw.startsWith("\x1b")) {
1442
- return { ...state, formInputBuf: formInputBuf + key.raw };
2107
+ // Preserve newlines in pasted text (multi-line supported), strip other control chars
2108
+ const clean = key.raw.replace(/[\x00-\x09\x0b\x0c\x0e-\x1f\x7f]/g, "");
2109
+ if (clean) {
2110
+ const newBuf = formInputBuf.slice(0, pos) + clean + formInputBuf.slice(pos);
2111
+ return { ...state, formInputBuf: newBuf, formInputCursorPos: pos + clean.length };
2112
+ }
1443
2113
  }
1444
2114
  return state;
1445
2115
  }
@@ -1447,8 +2117,11 @@ function handleResultsInput(state, key) {
1447
2117
  const { contentHeight } = getBoxDimensions();
1448
2118
  const contentLines = (state.error || state.result).split("\n");
1449
2119
  const visibleCount = Math.max(1, contentHeight - 3);
1450
- if (key.ctrl && key.name === "c")
1451
- return "exit";
2120
+ if (key.ctrl && key.name === "c") {
2121
+ if (state.quitConfirm)
2122
+ return "exit";
2123
+ return { ...state, quitConfirm: true };
2124
+ }
1452
2125
  if (key.name === "q" && !key.ctrl) {
1453
2126
  if (state.quitConfirm)
1454
2127
  return "exit";
@@ -1456,56 +2129,25 @@ function handleResultsInput(state, key) {
1456
2129
  }
1457
2130
  // Any other key cancels quit confirm
1458
2131
  const s = state.quitConfirm ? { ...state, quitConfirm: false } : state;
2132
+ const resultsClear = { result: "", error: "", resultScroll: 0, resultScrollX: 0 };
1459
2133
  const goBack = () => {
1460
- const resetFiltered = buildCommandList(s.tools);
1461
- const resetSel = selectableIndices(resetFiltered);
1462
- const searchReset = { searchQuery: "", searchCursorPos: 0, filteredItems: resetFiltered, listCursor: resetSel[0] ?? 0, listScrollTop: 0 };
1463
- const hasParams = s.selectedTool && Object.keys(s.selectedTool.inputSchema.properties || {}).length > 0;
2134
+ const isEmpty = !s.error && s.result !== EMPTY_LIST_SENTINEL && !s.result.trim();
2135
+ const hasParams = !isEmpty && s.selectedTool && Object.keys(s.selectedTool.inputSchema.properties || {}).length > 0;
1464
2136
  if (hasParams) {
2137
+ const f = filterFormFields(s.fields, "");
1465
2138
  return {
1466
- ...s, view: "form", result: "", error: "", resultScroll: 0, resultScrollX: 0,
2139
+ ...s, view: "form", ...resultsClear,
1467
2140
  formSearchQuery: "", formSearchCursorPos: 0,
1468
- formFilteredIndices: filterFormFields(s.fields, ""),
1469
- formListCursor: defaultFormCursor(s.fields, filterFormFields(s.fields, ""), s.formValues), formScrollTop: 0,
1470
- formEditing: false, formEditFieldIdx: -1, formShowRequired: false,
2141
+ formFilteredIndices: f,
2142
+ formListCursor: defaultFormCursor(s.fields, f, s.formValues), formScrollTop: 0,
2143
+ formEditing: false, formEditFieldIdx: -1, formShowRequired: false, formShowOptional: false,
1471
2144
  };
1472
2145
  }
1473
- return { ...s, view: "commands", selectedTool: null, result: "", error: "", resultScroll: 0, resultScrollX: 0, ...searchReset };
2146
+ return { ...s, ...commandListReset(s.tools), ...resultsClear };
1474
2147
  };
1475
- // Enter on success/empty-list/error screens go back
1476
- if (key.name === "return") {
1477
- const isEmpty = !s.error && s.result !== EMPTY_LIST_SENTINEL && !s.result.trim();
1478
- if (isEmpty) {
1479
- // Success screen → back to main menu
1480
- const resetFiltered = buildCommandList(s.tools);
1481
- const resetSel = selectableIndices(resetFiltered);
1482
- const searchReset = { searchQuery: "", searchCursorPos: 0, filteredItems: resetFiltered, listCursor: resetSel[0] ?? 0, listScrollTop: 0 };
1483
- return { ...s, view: "commands", selectedTool: null, result: "", error: "", resultScroll: 0, resultScrollX: 0, ...searchReset };
1484
- }
2148
+ if (key.name === "return" || key.name === "escape") {
1485
2149
  return goBack();
1486
2150
  }
1487
- if (key.name === "escape") {
1488
- const isEmpty = !s.error && s.result !== EMPTY_LIST_SENTINEL && !s.result.trim();
1489
- const resetFiltered = buildCommandList(s.tools);
1490
- const resetSel = selectableIndices(resetFiltered);
1491
- const searchReset = { searchQuery: "", searchCursorPos: 0, filteredItems: resetFiltered, listCursor: resetSel[0] ?? 0, listScrollTop: 0 };
1492
- if (isEmpty) {
1493
- // Success screen → back to main menu
1494
- return { ...s, view: "commands", selectedTool: null, result: "", error: "", resultScroll: 0, resultScrollX: 0, ...searchReset };
1495
- }
1496
- // Data or error → back to form if it has params, otherwise main menu
1497
- const hasParams = s.selectedTool && Object.keys(s.selectedTool.inputSchema.properties || {}).length > 0;
1498
- if (hasParams) {
1499
- return {
1500
- ...s, view: "form", result: "", error: "", resultScroll: 0, resultScrollX: 0,
1501
- formSearchQuery: "", formSearchCursorPos: 0,
1502
- formFilteredIndices: filterFormFields(s.fields, ""),
1503
- formListCursor: defaultFormCursor(s.fields, filterFormFields(s.fields, ""), s.formValues), formScrollTop: 0,
1504
- formEditing: false, formEditFieldIdx: -1, formShowRequired: false,
1505
- };
1506
- }
1507
- return { ...s, view: "commands", selectedTool: null, result: "", error: "", resultScroll: 0, resultScrollX: 0, ...searchReset };
1508
- }
1509
2151
  if (key.name === "up") {
1510
2152
  return { ...s, resultScroll: Math.max(0, s.resultScroll - 1) };
1511
2153
  }
@@ -1719,10 +2361,11 @@ export async function runApp(tools) {
1719
2361
  formEditFieldIdx: -1,
1720
2362
  formEditing: false,
1721
2363
  formInputBuf: "",
2364
+ formInputCursorPos: 0,
1722
2365
  formEnumCursor: 0,
1723
2366
  formEnumSelected: new Set(),
1724
2367
  formValues: {},
1725
- formShowRequired: false,
2368
+ formShowRequired: false, formShowOptional: false,
1726
2369
  formStack: [],
1727
2370
  dateParts: [],
1728
2371
  datePartCursor: 0,