@geometra/mcp 1.21.0 → 1.22.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/server.js +255 -4
- package/dist/session.d.ts +10 -0
- package/dist/session.js +25 -0
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -617,6 +617,73 @@ Equivalent to \`geometra_wait_for\` with \`present: false\` and \`text\` set to
|
|
|
617
617
|
return err(waited.error);
|
|
618
618
|
return ok(waitConditionSuccessLine(waited.value));
|
|
619
619
|
});
|
|
620
|
+
server.tool('geometra_wait_for_navigation', `Wait until the page URL changes and the new page's DOM stabilizes. Use this after clicking "Next", "Submit", or "Continue" on multi-page forms.
|
|
621
|
+
|
|
622
|
+
Captures the current URL, then polls until the URL changes and a stable UI tree is available. Returns the new page URL and a page model summary.`, {
|
|
623
|
+
timeoutMs: z
|
|
624
|
+
.number()
|
|
625
|
+
.int()
|
|
626
|
+
.min(500)
|
|
627
|
+
.max(60_000)
|
|
628
|
+
.optional()
|
|
629
|
+
.default(10_000)
|
|
630
|
+
.describe('Max time to wait for navigation + DOM stabilization (default 10s)'),
|
|
631
|
+
expectedUrl: z.string().optional().describe('Optional URL substring to match — keeps waiting if the URL changes to something else (e.g. intermediate redirects)'),
|
|
632
|
+
}, async ({ timeoutMs, expectedUrl }) => {
|
|
633
|
+
const session = getSession();
|
|
634
|
+
if (!session)
|
|
635
|
+
return err('Not connected. Call geometra_connect first.');
|
|
636
|
+
const beforeA11y = sessionA11y(session);
|
|
637
|
+
const beforeUrl = beforeA11y?.meta?.pageUrl ?? session.url;
|
|
638
|
+
const startedAt = performance.now();
|
|
639
|
+
let navigated = false;
|
|
640
|
+
while (performance.now() - startedAt < timeoutMs) {
|
|
641
|
+
await waitForUiCondition(session, () => {
|
|
642
|
+
const a = sessionA11y(session);
|
|
643
|
+
if (!a?.meta?.pageUrl)
|
|
644
|
+
return false;
|
|
645
|
+
const currentUrl = a.meta.pageUrl;
|
|
646
|
+
if (currentUrl === beforeUrl)
|
|
647
|
+
return false;
|
|
648
|
+
if (expectedUrl && !currentUrl.includes(expectedUrl))
|
|
649
|
+
return false;
|
|
650
|
+
return true;
|
|
651
|
+
}, Math.max(500, timeoutMs - (performance.now() - startedAt)));
|
|
652
|
+
const afterA11y = sessionA11y(session);
|
|
653
|
+
const afterUrl = afterA11y?.meta?.pageUrl;
|
|
654
|
+
if (afterUrl && afterUrl !== beforeUrl) {
|
|
655
|
+
if (!expectedUrl || afterUrl.includes(expectedUrl)) {
|
|
656
|
+
navigated = true;
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
const elapsedMs = Number((performance.now() - startedAt).toFixed(1));
|
|
662
|
+
const afterA11y = await sessionA11yWhenReady(session);
|
|
663
|
+
const afterUrl = afterA11y?.meta?.pageUrl;
|
|
664
|
+
if (!navigated) {
|
|
665
|
+
return err(JSON.stringify({
|
|
666
|
+
navigated: false,
|
|
667
|
+
beforeUrl,
|
|
668
|
+
currentUrl: afterUrl,
|
|
669
|
+
elapsedMs,
|
|
670
|
+
message: `URL did not change within ${timeoutMs}ms`,
|
|
671
|
+
}));
|
|
672
|
+
}
|
|
673
|
+
const model = afterA11y ? buildPageModel(afterA11y) : undefined;
|
|
674
|
+
return ok(JSON.stringify({
|
|
675
|
+
navigated: true,
|
|
676
|
+
beforeUrl,
|
|
677
|
+
afterUrl,
|
|
678
|
+
elapsedMs,
|
|
679
|
+
...(model ? {
|
|
680
|
+
summary: model.summary,
|
|
681
|
+
archetypes: model.archetypes,
|
|
682
|
+
formCount: model.forms.length,
|
|
683
|
+
...(model.captcha ? { captcha: model.captcha } : {}),
|
|
684
|
+
} : {}),
|
|
685
|
+
}));
|
|
686
|
+
});
|
|
620
687
|
server.tool('geometra_fill_fields', `Fill several labeled form fields in one MCP call. This is the preferred high-level primitive for long forms.
|
|
621
688
|
|
|
622
689
|
Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / comboboxes / radio-style questions addressed by field label + answer, \`"toggle"\` for individually labeled checkboxes or radios, and \`"file"\` for labeled uploads. When \`fieldId\` from \`geometra_form_schema\` is present, MCP can resolve the current label server-side so you do not need to duplicate \`fieldLabel\` / \`label\` for text, choice, and toggle fields.`, {
|
|
@@ -676,7 +743,8 @@ Use \`kind: "text"\` for textboxes / textareas, \`"choice"\` for selects / combo
|
|
|
676
743
|
}
|
|
677
744
|
catch (e) {
|
|
678
745
|
const message = e instanceof Error ? e.message : String(e);
|
|
679
|
-
|
|
746
|
+
const suggestion = isResolvedFillFieldInput(field) ? suggestRecovery(field, message) : undefined;
|
|
747
|
+
steps.push({ index, kind: field.kind, ok: false, error: message, ...(suggestion ? { suggestion } : {}) });
|
|
680
748
|
if (stopOnError) {
|
|
681
749
|
stoppedAt = index;
|
|
682
750
|
break;
|
|
@@ -732,8 +800,18 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
732
800
|
.min(0)
|
|
733
801
|
.optional()
|
|
734
802
|
.describe('Resume a partial fill from this field index (from a previous stoppedAt + 1). Skips already-filled fields.'),
|
|
803
|
+
verifyFills: z
|
|
804
|
+
.boolean()
|
|
805
|
+
.optional()
|
|
806
|
+
.default(false)
|
|
807
|
+
.describe('After filling, read back each field value and flag mismatches (e.g. autocomplete rejected input, format transformed). Adds a verification array to the response.'),
|
|
808
|
+
skipPreFilled: z
|
|
809
|
+
.boolean()
|
|
810
|
+
.optional()
|
|
811
|
+
.default(false)
|
|
812
|
+
.describe('Skip fields that already contain a matching value. Avoids overwriting good data from resume parsing or previous fills.'),
|
|
735
813
|
detail: detailInput(),
|
|
736
|
-
}, async ({ url, pageUrl, port, headless, width, height, slowMo, formId, valuesById, valuesByLabel, stopOnError, failOnInvalid, includeSteps, resumeFromIndex, detail }) => {
|
|
814
|
+
}, async ({ url, pageUrl, port, headless, width, height, slowMo, formId, valuesById, valuesByLabel, stopOnError, failOnInvalid, includeSteps, resumeFromIndex, verifyFills, skipPreFilled, detail }) => {
|
|
737
815
|
const directFields = !includeSteps && !formId && Object.keys(valuesById ?? {}).length === 0
|
|
738
816
|
? directLabelBatchFields(valuesByLabel)
|
|
739
817
|
: null;
|
|
@@ -818,6 +896,34 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
818
896
|
const planned = planFormFill(schema, { valuesById, valuesByLabel });
|
|
819
897
|
if (!planned.ok)
|
|
820
898
|
return err(planned.error);
|
|
899
|
+
let skippedCount = 0;
|
|
900
|
+
if (skipPreFilled) {
|
|
901
|
+
const schemaFieldById = new Map(schema.fields.map(f => [f.id, f]));
|
|
902
|
+
const indicesToRemove = new Set();
|
|
903
|
+
for (let i = 0; i < planned.planned.length; i++) {
|
|
904
|
+
const p = planned.planned[i];
|
|
905
|
+
const fieldId = p.field.fieldId;
|
|
906
|
+
if (!fieldId)
|
|
907
|
+
continue;
|
|
908
|
+
const schemaField = schemaFieldById.get(fieldId);
|
|
909
|
+
if (!schemaField?.value)
|
|
910
|
+
continue;
|
|
911
|
+
const currentVal = schemaField.value.toLowerCase().trim();
|
|
912
|
+
let intendedVal;
|
|
913
|
+
if (p.field.kind === 'text')
|
|
914
|
+
intendedVal = p.field.value;
|
|
915
|
+
else if (p.field.kind === 'choice')
|
|
916
|
+
intendedVal = p.field.value;
|
|
917
|
+
if (intendedVal && currentVal === intendedVal.toLowerCase().trim()) {
|
|
918
|
+
indicesToRemove.add(i);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
if (indicesToRemove.size > 0) {
|
|
922
|
+
skippedCount = indicesToRemove.size;
|
|
923
|
+
planned.planned = planned.planned.filter((_, i) => !indicesToRemove.has(i));
|
|
924
|
+
planned.fields = planned.planned.map(p => p.field);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
821
927
|
if (!includeSteps) {
|
|
822
928
|
let usedBatch = false;
|
|
823
929
|
let batchAckResult;
|
|
@@ -902,7 +1008,8 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
902
1008
|
}
|
|
903
1009
|
catch (e) {
|
|
904
1010
|
const message = e instanceof Error ? e.message : String(e);
|
|
905
|
-
|
|
1011
|
+
const suggestion = suggestRecovery(field, message);
|
|
1012
|
+
steps.push({ index, kind: field.kind, ok: false, ...(confidence !== undefined ? { confidence, matchMethod } : {}), error: message, ...(suggestion ? { suggestion } : {}) });
|
|
906
1013
|
if (stopOnError) {
|
|
907
1014
|
stoppedAt = index;
|
|
908
1015
|
break;
|
|
@@ -914,6 +1021,7 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
914
1021
|
const invalidRemaining = signals?.invalidFields.length ?? 0;
|
|
915
1022
|
const successCount = steps.filter(step => step.ok === true).length;
|
|
916
1023
|
const errorCount = steps.length - successCount;
|
|
1024
|
+
const verification = verifyFills ? verifyFormFills(session, planned.planned) : undefined;
|
|
917
1025
|
const payload = {
|
|
918
1026
|
...connection,
|
|
919
1027
|
completed: stoppedAt === undefined && (startIndex + steps.length) === planned.fields.length,
|
|
@@ -923,12 +1031,14 @@ Pass \`valuesById\` with field ids from \`geometra_form_schema\` for the most st
|
|
|
923
1031
|
fieldCount: planned.fields.length,
|
|
924
1032
|
successCount,
|
|
925
1033
|
errorCount,
|
|
1034
|
+
...(skippedCount > 0 ? { skippedPreFilled: skippedCount } : {}),
|
|
926
1035
|
minConfidence: planned.planned.length > 0
|
|
927
1036
|
? Number(Math.min(...planned.planned.map(p => p.confidence)).toFixed(2))
|
|
928
1037
|
: undefined,
|
|
929
1038
|
...(startIndex > 0 ? { resumedFromIndex: startIndex } : {}),
|
|
930
1039
|
...(includeSteps ? { steps } : {}),
|
|
931
1040
|
...(stoppedAt !== undefined ? { stoppedAt, resumeFromIndex: stoppedAt + 1 } : {}),
|
|
1041
|
+
...(verification ? { verification } : {}),
|
|
932
1042
|
...(signals ? { final: sessionSignalsPayload(signals, detail) } : {}),
|
|
933
1043
|
};
|
|
934
1044
|
recordWorkflowFill(session, schema.formId, schema.name, valuesById, valuesByLabel, invalidRemaining, planned.fields.length);
|
|
@@ -1865,6 +1975,7 @@ function packedFormSchemas(forms) {
|
|
|
1865
1975
|
...(field.checked !== undefined ? { c: field.checked ? 1 : 0 } : {}),
|
|
1866
1976
|
...(field.values && field.values.length > 0 ? { vs: field.values } : {}),
|
|
1867
1977
|
...(field.aliases ? { al: field.aliases } : {}),
|
|
1978
|
+
...(field.format ? { fmt: field.format } : {}),
|
|
1868
1979
|
...(field.context ? { x: field.context } : {}),
|
|
1869
1980
|
})),
|
|
1870
1981
|
...(form.sections ? { s: form.sections.map(s => ({ n: s.name, fi: s.fieldIds })) } : {}),
|
|
@@ -2458,11 +2569,86 @@ function coerceChoiceValue(field, value) {
|
|
|
2458
2569
|
const option = field.options?.find(option => normalizeLookupKey(option) === desired);
|
|
2459
2570
|
return option ?? (value ? 'Yes' : 'No');
|
|
2460
2571
|
}
|
|
2572
|
+
/**
|
|
2573
|
+
* Normalize a text value based on field format hints (placeholder, inputType, pattern).
|
|
2574
|
+
* Handles common date and phone format conversions.
|
|
2575
|
+
*/
|
|
2576
|
+
function normalizeFieldValue(value, format) {
|
|
2577
|
+
if (!format)
|
|
2578
|
+
return value;
|
|
2579
|
+
// Date normalization: detect ISO dates and convert to placeholder format
|
|
2580
|
+
if (format.inputType === 'date')
|
|
2581
|
+
return value; // native date inputs handle ISO fine
|
|
2582
|
+
const isDateLike = /^\d{4}-\d{2}-\d{2}$/.test(value) || /^\d{1,2}[/\-.]\d{1,2}[/\-.]\d{2,4}$/.test(value);
|
|
2583
|
+
if (isDateLike && format.placeholder) {
|
|
2584
|
+
const parsed = parseDateLoose(value);
|
|
2585
|
+
if (parsed) {
|
|
2586
|
+
const ph = format.placeholder.toLowerCase();
|
|
2587
|
+
if (ph.includes('mm/dd/yyyy') || ph.includes('mm/dd/yy')) {
|
|
2588
|
+
return `${pad2(parsed.month)}/${pad2(parsed.day)}/${parsed.year}`;
|
|
2589
|
+
}
|
|
2590
|
+
if (ph.includes('dd/mm/yyyy') || ph.includes('dd/mm/yy')) {
|
|
2591
|
+
return `${pad2(parsed.day)}/${pad2(parsed.month)}/${parsed.year}`;
|
|
2592
|
+
}
|
|
2593
|
+
if (ph.includes('yyyy-mm-dd')) {
|
|
2594
|
+
return `${parsed.year}-${pad2(parsed.month)}-${pad2(parsed.day)}`;
|
|
2595
|
+
}
|
|
2596
|
+
if (ph.includes('mm-dd-yyyy')) {
|
|
2597
|
+
return `${pad2(parsed.month)}-${pad2(parsed.day)}-${parsed.year}`;
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
// Phone normalization
|
|
2602
|
+
if (format.inputType === 'tel' || format.autocomplete?.includes('tel')) {
|
|
2603
|
+
const digits = value.replace(/\D/g, '');
|
|
2604
|
+
if (digits.length === 10 && format.placeholder) {
|
|
2605
|
+
const ph = format.placeholder;
|
|
2606
|
+
if (ph.includes('(') && ph.includes(')')) {
|
|
2607
|
+
return `(${digits.slice(0, 3)}) ${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
2608
|
+
}
|
|
2609
|
+
if (ph.includes('-') && !ph.includes('(')) {
|
|
2610
|
+
return `${digits.slice(0, 3)}-${digits.slice(3, 6)}-${digits.slice(6)}`;
|
|
2611
|
+
}
|
|
2612
|
+
if (ph.includes('.')) {
|
|
2613
|
+
return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6)}`;
|
|
2614
|
+
}
|
|
2615
|
+
}
|
|
2616
|
+
if (digits.length === 11 && digits.startsWith('1') && format.placeholder) {
|
|
2617
|
+
const core = digits.slice(1);
|
|
2618
|
+
const ph = format.placeholder;
|
|
2619
|
+
if (ph.startsWith('+1') || ph.startsWith('1')) {
|
|
2620
|
+
return `+1 (${core.slice(0, 3)}) ${core.slice(3, 6)}-${core.slice(6)}`;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
return value;
|
|
2625
|
+
}
|
|
2626
|
+
function parseDateLoose(value) {
|
|
2627
|
+
// YYYY-MM-DD
|
|
2628
|
+
const iso = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
2629
|
+
if (iso)
|
|
2630
|
+
return { year: +iso[1], month: +iso[2], day: +iso[3] };
|
|
2631
|
+
// MM/DD/YYYY or MM-DD-YYYY or MM.DD.YYYY
|
|
2632
|
+
const mdy = value.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{4})$/);
|
|
2633
|
+
if (mdy)
|
|
2634
|
+
return { year: +mdy[3], month: +mdy[1], day: +mdy[2] };
|
|
2635
|
+
// MM/DD/YY
|
|
2636
|
+
const mdy2 = value.match(/^(\d{1,2})[/\-.](\d{1,2})[/\-.](\d{2})$/);
|
|
2637
|
+
if (mdy2) {
|
|
2638
|
+
const yr = +mdy2[3];
|
|
2639
|
+
return { year: yr < 50 ? 2000 + yr : 1900 + yr, month: +mdy2[1], day: +mdy2[2] };
|
|
2640
|
+
}
|
|
2641
|
+
return null;
|
|
2642
|
+
}
|
|
2643
|
+
function pad2(n) {
|
|
2644
|
+
return n < 10 ? `0${n}` : String(n);
|
|
2645
|
+
}
|
|
2461
2646
|
function plannedFillInputsForField(field, value) {
|
|
2462
2647
|
if (field.kind === 'text') {
|
|
2463
2648
|
if (typeof value !== 'string')
|
|
2464
2649
|
return { error: `Field "${field.label}" expects a string value` };
|
|
2465
|
-
|
|
2650
|
+
const normalized = normalizeFieldValue(value, field.format);
|
|
2651
|
+
return [{ kind: 'text', fieldId: field.id, fieldLabel: field.label, value: normalized }];
|
|
2466
2652
|
}
|
|
2467
2653
|
if (field.kind === 'choice') {
|
|
2468
2654
|
const coerced = coerceChoiceValue(field, value);
|
|
@@ -3060,6 +3246,71 @@ async function executeBatchAction(session, action, detail, includeSteps) {
|
|
|
3060
3246
|
}
|
|
3061
3247
|
}
|
|
3062
3248
|
}
|
|
3249
|
+
function suggestRecovery(field, error) {
|
|
3250
|
+
const lowerError = error.toLowerCase();
|
|
3251
|
+
if (field.kind === 'choice') {
|
|
3252
|
+
if (lowerError.includes('no option') || lowerError.includes('not found') || lowerError.includes('visible')) {
|
|
3253
|
+
return `Try geometra_pick_listbox_option with fieldLabel="${field.fieldLabel}" and label="${field.value}" for custom dropdowns.`;
|
|
3254
|
+
}
|
|
3255
|
+
if (lowerError.includes('timeout')) {
|
|
3256
|
+
return `Dropdown may load options asynchronously. Retry with a higher timeoutMs, or use geometra_pick_listbox_option with a query parameter.`;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
if (field.kind === 'text') {
|
|
3260
|
+
if (lowerError.includes('format') || lowerError.includes('pattern') || lowerError.includes('invalid')) {
|
|
3261
|
+
return `Field may require a specific format (e.g. MM/DD/YYYY for dates, (555) 123-4567 for phone). Check the field's placeholder or aria-describedby for format hints.`;
|
|
3262
|
+
}
|
|
3263
|
+
if (lowerError.includes('not found') || lowerError.includes('no match')) {
|
|
3264
|
+
return `Field label "${field.fieldLabel}" may have changed after page update. Refresh with geometra_form_schema and retry.`;
|
|
3265
|
+
}
|
|
3266
|
+
if (lowerError.includes('timeout')) {
|
|
3267
|
+
return `Field may be disabled or obscured. Check geometra_snapshot for the field's current state.`;
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
if (field.kind === 'toggle') {
|
|
3271
|
+
if (lowerError.includes('not found') || lowerError.includes('no match')) {
|
|
3272
|
+
return `Checkbox/radio "${field.label}" not found. The label may be dynamic — try geometra_set_checked with exact=false.`;
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
3275
|
+
if (field.kind === 'file') {
|
|
3276
|
+
if (lowerError.includes('no file') || lowerError.includes('not found')) {
|
|
3277
|
+
return `File input not found by label. Try geometra_upload_files with strategy="hidden" or provide click coordinates.`;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
if (lowerError.includes('timeout')) {
|
|
3281
|
+
return `Action timed out. The page may still be loading. Try geometra_wait_for with a loading indicator, then retry.`;
|
|
3282
|
+
}
|
|
3283
|
+
return undefined;
|
|
3284
|
+
}
|
|
3285
|
+
function verifyFormFills(session, planned) {
|
|
3286
|
+
const a11y = sessionA11y(session);
|
|
3287
|
+
if (!a11y)
|
|
3288
|
+
return { verified: 0, mismatches: [] };
|
|
3289
|
+
const mismatches = [];
|
|
3290
|
+
let verified = 0;
|
|
3291
|
+
for (const p of planned) {
|
|
3292
|
+
if (p.field.kind === 'toggle' || p.field.kind === 'file')
|
|
3293
|
+
continue;
|
|
3294
|
+
const label = p.field.fieldLabel;
|
|
3295
|
+
const expected = p.field.kind === 'text' ? p.field.value : p.field.value;
|
|
3296
|
+
const matches = [
|
|
3297
|
+
...findNodes(a11y, { name: label, role: 'textbox' }),
|
|
3298
|
+
...findNodes(a11y, { name: label, role: 'combobox' }),
|
|
3299
|
+
];
|
|
3300
|
+
const match = matches[0];
|
|
3301
|
+
const actual = match?.value?.trim();
|
|
3302
|
+
if (!actual || !expected) {
|
|
3303
|
+
mismatches.push({ fieldLabel: label, expected, actual, ...(p.field.fieldId ? { fieldId: p.field.fieldId } : {}) });
|
|
3304
|
+
}
|
|
3305
|
+
else if (actual.toLowerCase() !== expected.toLowerCase()) {
|
|
3306
|
+
mismatches.push({ fieldLabel: label, expected, actual, ...(p.field.fieldId ? { fieldId: p.field.fieldId } : {}) });
|
|
3307
|
+
}
|
|
3308
|
+
else {
|
|
3309
|
+
verified++;
|
|
3310
|
+
}
|
|
3311
|
+
}
|
|
3312
|
+
return { verified, mismatches };
|
|
3313
|
+
}
|
|
3063
3314
|
async function executeFillField(session, field, detail) {
|
|
3064
3315
|
switch (field.kind) {
|
|
3065
3316
|
case 'text': {
|
package/dist/session.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export interface A11yNode {
|
|
|
29
29
|
scrollX?: number;
|
|
30
30
|
scrollY?: number;
|
|
31
31
|
controlTag?: string;
|
|
32
|
+
placeholder?: string;
|
|
33
|
+
inputPattern?: string;
|
|
34
|
+
inputType?: string;
|
|
35
|
+
autocomplete?: string;
|
|
32
36
|
};
|
|
33
37
|
bounds: {
|
|
34
38
|
x: number;
|
|
@@ -270,6 +274,12 @@ export interface FormSchemaField {
|
|
|
270
274
|
optionCount?: number;
|
|
271
275
|
options?: string[];
|
|
272
276
|
aliases?: Record<string, string[]>;
|
|
277
|
+
format?: {
|
|
278
|
+
placeholder?: string;
|
|
279
|
+
pattern?: string;
|
|
280
|
+
inputType?: string;
|
|
281
|
+
autocomplete?: string;
|
|
282
|
+
};
|
|
273
283
|
context?: NodeContextModel;
|
|
274
284
|
}
|
|
275
285
|
export interface FormSchemaSection {
|
package/dist/session.js
CHANGED
|
@@ -1597,6 +1597,21 @@ function computeOptionAliases(options) {
|
|
|
1597
1597
|
}
|
|
1598
1598
|
return Object.keys(result).length > 0 ? result : undefined;
|
|
1599
1599
|
}
|
|
1600
|
+
function buildFieldFormat(node) {
|
|
1601
|
+
const m = node.meta;
|
|
1602
|
+
if (!m)
|
|
1603
|
+
return undefined;
|
|
1604
|
+
const format = {};
|
|
1605
|
+
if (m.placeholder)
|
|
1606
|
+
format.placeholder = m.placeholder;
|
|
1607
|
+
if (m.inputPattern)
|
|
1608
|
+
format.pattern = m.inputPattern;
|
|
1609
|
+
if (m.inputType)
|
|
1610
|
+
format.inputType = m.inputType;
|
|
1611
|
+
if (m.autocomplete)
|
|
1612
|
+
format.autocomplete = m.autocomplete;
|
|
1613
|
+
return Object.keys(format).length > 0 ? format : undefined;
|
|
1614
|
+
}
|
|
1600
1615
|
function simpleSchemaField(root, node) {
|
|
1601
1616
|
const context = nodeContextForNode(root, node);
|
|
1602
1617
|
const label = fieldLabel(node) ?? sanitizeInlineName(node.name, 80) ?? context?.prompt;
|
|
@@ -1607,6 +1622,7 @@ function simpleSchemaField(root, node) {
|
|
|
1607
1622
|
? 'select'
|
|
1608
1623
|
: 'listbox'
|
|
1609
1624
|
: undefined;
|
|
1625
|
+
const format = buildFieldFormat(node);
|
|
1610
1626
|
return {
|
|
1611
1627
|
id: formFieldIdForPath(node.path),
|
|
1612
1628
|
kind: node.role === 'combobox' ? 'choice' : 'text',
|
|
@@ -1615,6 +1631,7 @@ function simpleSchemaField(root, node) {
|
|
|
1615
1631
|
...(node.state?.required ? { required: true } : {}),
|
|
1616
1632
|
...(node.state?.invalid ? { invalid: true } : {}),
|
|
1617
1633
|
...compactSchemaValue(node.value, 72),
|
|
1634
|
+
...(format ? { format } : {}),
|
|
1618
1635
|
...(compactSchemaContext(context, label) ? { context: compactSchemaContext(context, label) } : {}),
|
|
1619
1636
|
};
|
|
1620
1637
|
}
|
|
@@ -2473,6 +2490,14 @@ function walkNode(element, layout, path) {
|
|
|
2473
2490
|
meta.scrollY = semantic.scrollY;
|
|
2474
2491
|
if (typeof semantic?.tag === 'string' && semantic.tag.trim().length > 0)
|
|
2475
2492
|
meta.controlTag = semantic.tag;
|
|
2493
|
+
if (typeof semantic?.placeholder === 'string')
|
|
2494
|
+
meta.placeholder = semantic.placeholder;
|
|
2495
|
+
if (typeof semantic?.inputPattern === 'string')
|
|
2496
|
+
meta.inputPattern = semantic.inputPattern;
|
|
2497
|
+
if (typeof semantic?.inputType === 'string')
|
|
2498
|
+
meta.inputType = semantic.inputType;
|
|
2499
|
+
if (typeof semantic?.autocomplete === 'string')
|
|
2500
|
+
meta.autocomplete = semantic.autocomplete;
|
|
2476
2501
|
const children = [];
|
|
2477
2502
|
const elementChildren = element.children;
|
|
2478
2503
|
const layoutChildren = layout.children;
|
package/package.json
CHANGED