From ad0227c9dd6d596b40fd89da8b3a43782cadfe57 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Thu, 9 Jul 2026 18:53:58 -0500 Subject: [PATCH] refactor(form): rename link fields to external --- .../client/src/promise/generated/types.ts | 36 +++++------ packages/core/src/form.ts | 10 +-- packages/core/src/mcp/index.ts | 2 +- packages/core/test/form.test.ts | 12 ++-- packages/schema/src/form.ts | 12 ++-- packages/schema/test/contract-hygiene.test.ts | 6 +- packages/sdk/js/src/v2/gen/types.gen.ts | 8 +-- packages/tui/src/routes/session/form.tsx | 62 +++++++++---------- .../test/cli/cmd/tui/notifications.test.ts | 7 +-- packages/tui/test/cli/tui/data.test.tsx | 9 ++- 10 files changed, 84 insertions(+), 80 deletions(-) diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 23c046f36db..b74f223006e 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -2510,7 +2510,7 @@ export type FormRequestListOutput = { custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } ), ...Array< | { @@ -2593,7 +2593,7 @@ export type FormRequestListOutput = { custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } >, ] }> @@ -2689,7 +2689,7 @@ export type FormListOutput = { custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } ), ...Array< | { @@ -2772,7 +2772,7 @@ export type FormListOutput = { custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } >, ] }> @@ -2874,7 +2874,7 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + | { readonly type: "external"; readonly url: string; readonly title?: string; readonly description?: string } ), ...Array< | { @@ -2965,7 +2965,7 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + | { readonly type: "external"; readonly url: string; readonly title?: string; readonly description?: string } >, ] }["id"] @@ -3063,7 +3063,7 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + | { readonly type: "external"; readonly url: string; readonly title?: string; readonly description?: string } ), ...Array< | { @@ -3154,7 +3154,7 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + | { readonly type: "external"; readonly url: string; readonly title?: string; readonly description?: string } >, ] }["title"] @@ -3252,7 +3252,7 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + | { readonly type: "external"; readonly url: string; readonly title?: string; readonly description?: string } ), ...Array< | { @@ -3343,7 +3343,7 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + | { readonly type: "external"; readonly url: string; readonly title?: string; readonly description?: string } >, ] }["metadata"] @@ -3441,7 +3441,7 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + | { readonly type: "external"; readonly url: string; readonly title?: string; readonly description?: string } ), ...Array< | { @@ -3532,7 +3532,7 @@ export type FormCreateInput = { readonly custom?: boolean readonly default?: ReadonlyArray } - | { readonly type: "link"; readonly url: string; readonly title?: string; readonly description?: string } + | { readonly type: "external"; readonly url: string; readonly title?: string; readonly description?: string } >, ] }["fields"] @@ -3626,7 +3626,7 @@ export type FormCreateOutput = { custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } ), ...Array< | { @@ -3709,7 +3709,7 @@ export type FormCreateOutput = { custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } >, ] } @@ -3808,7 +3808,7 @@ export type FormGetOutput = { custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } ), ...Array< | { @@ -3891,7 +3891,7 @@ export type FormGetOutput = { custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } >, ] } @@ -5429,7 +5429,7 @@ export type EventSubscribeOutput = custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } ), ...Array< | { @@ -5492,7 +5492,7 @@ export type EventSubscribeOutput = custom?: boolean default?: Array } - | { type: "link"; url: string; title?: string; description?: string } + | { type: "external"; url: string; title?: string; description?: string } >, ] } diff --git a/packages/core/src/form.ts b/packages/core/src/form.ts index 6f7ab3f8b0a..8c810e6c8ae 100644 --- a/packages/core/src/form.ts +++ b/packages/core/src/form.ts @@ -224,12 +224,14 @@ export const locationLayer = layer export const node = makeLocationNode({ service: Service, layer, deps: [EventV2.node] }) function validateAnswer(form: Info, answer: Answer) { - const fields = new Map(form.fields.flatMap((field) => (field.type === "link" ? [] : [[field.key, field] as const]))) + const fields = new Map( + form.fields.flatMap((field) => (field.type === "external" ? [] : [[field.key, field] as const])), + ) for (const key of Object.keys(answer)) { if (!fields.has(key)) return `Unknown form field: ${key}` } for (const field of form.fields) { - if (field.type === "link") continue + if (field.type === "external") continue const value = answer[field.key] const active = isActive(field, answer) if (value === undefined) { @@ -242,7 +244,7 @@ function validateAnswer(form: Info, answer: Answer) { } } -type InputField = Exclude +type InputField = Exclude function isActive(field: InputField, answer: Answer) { if (!field.when) return true @@ -265,7 +267,7 @@ function validateFields(fields: ReadonlyArray) { if (fields.length === 0) return "Form must have at least one field" const earlier = new Map() for (const field of fields) { - if (field.type === "link") continue + if (field.type === "external") continue if (earlier.has(field.key)) return `Duplicate form field key: ${field.key}` for (const when of field.when ?? []) { const target = earlier.get(when.key) diff --git a/packages/core/src/mcp/index.ts b/packages/core/src/mcp/index.ts index 370d6e6084c..0da4ed01eeb 100644 --- a/packages/core/src/mcp/index.ts +++ b/packages/core/src/mcp/index.ts @@ -311,7 +311,7 @@ export const layer = Layer.effect( elicitationID: input.params.elicitationId, message: input.params.message, }, - fields: [{ type: "link", url: input.params.url }], + fields: [{ type: "external", url: input.params.url }], }) .pipe( Effect.raceFirst(waitForAbort(input.signal)), diff --git a/packages/core/test/form.test.ts b/packages/core/test/form.test.ts index 0bce11000cf..44893d3b826 100644 --- a/packages/core/test/form.test.ts +++ b/packages/core/test/form.test.ts @@ -58,13 +58,13 @@ describe("Form", () => { yield* service.reply({ id: created.id, answer: { name: "Ava" } }) expect(yield* service.state(created.id)).toEqual({ status: "answered", answer: { name: "Ava" } }) - const linkOnly = yield* service.create({ + const externalOnly = yield* service.create({ sessionID: "global", title: "External setup", - fields: [{ type: "link", url: "https://example.com/setup" }], + fields: [{ type: "external", url: "https://example.com/setup" }], }) - yield* service.reply({ id: linkOnly.id, answer: {} }) - expect(yield* service.state(linkOnly.id)).toEqual({ status: "answered", answer: {} }) + yield* service.reply({ id: externalOnly.id, answer: {} }) + expect(yield* service.state(externalOnly.id)).toEqual({ status: "answered", answer: {} }) }), ) @@ -267,14 +267,14 @@ describe("Form", () => { }), ) - it.effect("treats link fields as non-answerable", () => + it.effect("treats external fields as non-answerable", () => Effect.gen(function* () { const service = yield* Form.Service const created = yield* service.create({ sessionID: "global", title: "External setup", fields: [ - { type: "link", url: "https://example.com/setup", title: "Open setup" }, + { type: "external", url: "https://example.com/setup", title: "Open setup" }, { key: "name", type: "string", required: true }, ], }) diff --git a/packages/schema/src/form.ts b/packages/schema/src/form.ts index caae0c0c043..c12620b0274 100644 --- a/packages/schema/src/form.ts +++ b/packages/schema/src/form.ts @@ -94,13 +94,13 @@ export const MultiselectField = Schema.Struct({ }).annotate({ identifier: "Form.MultiselectField" }) export interface MultiselectField extends Schema.Schema.Type {} -export const LinkField = Schema.Struct({ - type: Schema.Literal("link"), +export const ExternalField = Schema.Struct({ + type: Schema.Literal("external"), url: Schema.String, title: Schema.String.pipe(optional), description: Schema.String.pipe(optional), -}).annotate({ identifier: "Form.LinkField" }) -export interface LinkField extends Schema.Schema.Type {} +}).annotate({ identifier: "Form.ExternalField" }) +export interface ExternalField extends Schema.Schema.Type {} export const Field = Schema.Union([ StringField, @@ -108,9 +108,9 @@ export const Field = Schema.Union([ IntegerField, BooleanField, MultiselectField, - LinkField, + ExternalField, ]).pipe(Schema.toTaggedUnion("type"), Schema.annotate({ identifier: "Form.Field" })) -export type Field = StringField | NumberField | IntegerField | BooleanField | MultiselectField | LinkField +export type Field = StringField | NumberField | IntegerField | BooleanField | MultiselectField | ExternalField export const Fields = Schema.NonEmptyArray(Field).annotate({ identifier: "Form.Fields" }) export type Fields = typeof Fields.Type diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index a3d32b95b18..8a0484f692c 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -61,8 +61,8 @@ describe("contract hygiene", () => { Schema.decodeUnknownSync(Form.Info)({ id: Form.ID.create(), sessionID: "global", - title: "Link form", - fields: [{ type: "link", url: "https://example.com" }], + title: "External form", + fields: [{ type: "external", url: "https://example.com" }], }).fields, ).toHaveLength(1) }) @@ -92,7 +92,7 @@ describe("contract hygiene", () => { Form.Field, Form.Fields, Form.Info, - Form.LinkField, + Form.ExternalField, Mcp.Resource, Mcp.ResourceTemplate, Mcp.ResourceCatalog, diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index aabedb6f4ca..64c1297d873 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -3545,8 +3545,8 @@ export type FormMultiselectField = { default?: Array } -export type FormLinkField = { - type: "link" +export type FormExternalField = { + type: "external" url: string title?: string description?: string @@ -3558,7 +3558,7 @@ export type FormField = | FormIntegerField | FormBooleanField | FormMultiselectField - | FormLinkField + | FormExternalField export type FormFields = Array @@ -10622,7 +10622,7 @@ export type FormField1 = | FormIntegerField1 | FormBooleanField1 | FormMultiselectField1 - | FormLinkField + | FormExternalField export type FormFields1 = [FormField1, FormField1] diff --git a/packages/tui/src/routes/session/form.tsx b/packages/tui/src/routes/session/form.tsx index 174fe784903..4620a4ee695 100644 --- a/packages/tui/src/routes/session/form.tsx +++ b/packages/tui/src/routes/session/form.tsx @@ -15,19 +15,19 @@ import { useBindings, useOpencodeModeStack } from "../../keymap" const FORM_MODE = "form" -type Field = Exclude -type LinkField = Extract +type Field = Exclude +type ExternalField = Extract function isField(field: FormField): field is Field { - return field.type !== "link" + return field.type !== "external" } -function isLink(field: FormField): field is LinkField { - return field.type === "link" +function isExternal(field: FormField): field is ExternalField { + return field.type === "external" } function fieldLabel(field: FormField) { - return field.title ?? (field.type === "link" ? field.url : field.key) + return field.title ?? (field.type === "external" ? field.url : field.key) } function truncate(label: string, max: number) { @@ -193,7 +193,7 @@ function FieldsPrompt(props: { form: FormInfo }) { const fields = createMemo(() => { const answers: Record = {} return props.form.fields.filter((field) => { - if (field.type === "link") return true + if (field.type === "external") return true const active = (field.when ?? []).every((when) => { const value = answers[when.key] if (value === undefined) return false @@ -208,7 +208,7 @@ function FieldsPrompt(props: { form: FormInfo }) { const list = fields() if (list.length !== 1) return false const field = list[0]! - if (field.type === "link") return false + if (field.type === "external") return false return field.type === "boolean" || (field.type === "string" && field.options !== undefined) }) const answerable = createMemo(() => fields().filter(isField)) @@ -229,9 +229,9 @@ function FieldsPrompt(props: { form: FormInfo }) { const current = field() return current && isField(current) ? current : undefined }) - const linkField = createMemo(() => { + const externalField = createMemo(() => { const current = field() - return current && isLink(current) ? current : undefined + return current && isExternal(current) ? current : undefined }) const confirm = createMemo(() => !single() && store.tab >= fields().length) const rows = createMemo(() => { @@ -263,7 +263,7 @@ function FieldsPrompt(props: { form: FormInfo }) { const multi = createMemo(() => answerField()?.type === "multiselect") const actionLabel = createMemo(() => { if (confirm()) return answerable().length === 0 ? "I finished" : "submit" - if (linkField()) return "open link" + if (externalField()) return "open link" if (multi()) return "toggle" if (single()) return "submit" return "confirm" @@ -487,8 +487,8 @@ function FieldsPrompt(props: { form: FormInfo }) { void sdk.api.form.cancel({ sessionID: props.form.sessionID, formID: props.form.id }, requestOptions(props.form)) } - function openLink() { - const current = linkField() + function openExternal() { + const current = externalField() if (!current) return const index = store.tab setStore("error", "") @@ -497,8 +497,8 @@ function FieldsPrompt(props: { form: FormInfo }) { .catch(() => setStore("error", "Could not open the browser. Copy the URL and continue manually.")) } - function copyLink() { - const current = linkField() + function copyExternal() { + const current = externalField() if (!current || !clipboard.write) return void clipboard .write(current.url) @@ -657,10 +657,10 @@ function FieldsPrompt(props: { form: FormInfo }) { group: "Form", cmd: () => selectTab((store.tab - 1 + tabs()) % tabs()), }, - ...(linkField() + ...(externalField() ? [ - { key: "return", desc: "Open link", group: "Form", cmd: openLink }, - { key: "c", desc: "Copy link", group: "Form", cmd: copyLink }, + { key: "return", desc: "Open link", group: "Form", cmd: openExternal }, + { key: "c", desc: "Copy link", group: "Form", cmd: copyExternal }, { key: "escape", desc: "Dismiss form", group: "Form", cmd: cancel }, ...tuiConfig.keybinds.get("app.exit"), ] @@ -764,7 +764,7 @@ function FieldsPrompt(props: { form: FormInfo }) { {(item, index) => { const isTab = () => index() === store.tab - const isAnswered = () => item.type !== "link" && store.answers[item.key] !== undefined + const isAnswered = () => item.type !== "external" && store.answers[item.key] !== undefined return ( - - {(link) => ( + + {(external) => ( - - {link().title} + + {external().title} - - {link().description} + + {external().description} { if (renderer.getSelection()?.getSelectedText()) return - openLink() + openExternal() }} > - {link().url} + {external().url} )} @@ -1025,7 +1025,7 @@ function FieldsPrompt(props: { form: FormInfo }) { {"⇆"} tab - + {"↑↓"} select @@ -1040,13 +1040,13 @@ function FieldsPrompt(props: { form: FormInfo }) { onMouseUp={() => { if (renderer.getSelection()?.getSelectedText()) return if (confirm()) submit() - if (linkField()) openLink() + if (externalField()) openExternal() }} > enter {actionLabel()} - - + + c copy diff --git a/packages/tui/test/cli/cmd/tui/notifications.test.ts b/packages/tui/test/cli/cmd/tui/notifications.test.ts index 64611066014..efd46c38e8f 100644 --- a/packages/tui/test/cli/cmd/tui/notifications.test.ts +++ b/packages/tui/test/cli/cmd/tui/notifications.test.ts @@ -77,15 +77,12 @@ function question(id: string, sessionID = "session"): QuestionRequest { } } -function form( - id: string, - sessionID = "session", -): Extract["data"]["form"] { +function form(id: string, sessionID = "session"): Extract["data"]["form"] { return { id, sessionID, title: "Input requested", - fields: [{ type: "link", url: "https://example.com" }], + fields: [{ type: "external", url: "https://example.com" }], } } diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index 16e7a746d07..8360b952923 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -12,7 +12,12 @@ import { createSessionRows, type SessionRow } from "../../../src/routes/session/ import { createApi, createClient, createEventStream, createFetch, directory, json } from "../../fixture/tui-sdk" import { TestTuiContexts } from "../../fixture/tui-environment" -const formFields = [{ type: "link", url: "https://example.com" }] satisfies [{ type: "link"; url: string }] +const formFields = [{ type: "external", url: "https://example.com" }] satisfies [ + { + type: "external" + url: string + }, +] async function wait(fn: () => boolean, timeout = 2000) { const start = Date.now() @@ -1808,7 +1813,7 @@ test("reconciles all pending form requests when the event stream reconnects", as id: "frm_keep", sessionID: "ses_keep", title: "Input requested", - fields: [{ type: "link" as const, url: "https://example.com" }], + fields: [{ type: "external" as const, url: "https://example.com" }], }, ] let calls = 0