Skip to content

feat(P060): rename REMOVE_FIELDS->DELETE_FIELDS and DELETE_PAGE->DELETE_PAGES (array)#33

Merged
bendersej merged 11 commits intomainfrom
copilot-improvements
Apr 27, 2026
Merged

feat(P060): rename REMOVE_FIELDS->DELETE_FIELDS and DELETE_PAGE->DELETE_PAGES (array)#33
bendersej merged 11 commits intomainfrom
copilot-improvements

Conversation

@bendersej
Copy link
Copy Markdown
Member

@bendersej bendersej commented Apr 27, 2026

Background

Cleanup pass on the copilot bridge + LLM-tool adapter. Pairs with the matching top-level repo PR (SimplePDF/simple-pdf#196) which carries the editor / e2e changes.

The bridge now owns the iframe contract end-to-end (Zod schemas + descriptions + parsing). The adapter is a one-line-per-tool router.

Changes

Bridge owns the contract

  • New embed-bridge/schemas.ts — single source of truth: one Zod schema per iframe operation, each with .describe(). Snake_case keys throughout (matches the wire).
  • IframeBridge methods accept unknown and validate internally via parseAndSend(Schema, 'EVENT', args). Bad input → bad_input without a postMessage round-trip.
  • BridgeResult.error.code is now a typed union with autocomplete for bridge-owned codes.

Adapter is thin

  • dispatch.ts, safeDispatch, client-tools/schemas.ts, and the redundant isClientToolName runtime narrow are gone.
  • New tools.ts enumerates LLM tool names and pulls descriptions verbatim from the bridge schemas (no duplicated text).
  • factory.ts switch terminus is pure routing — every arm is one line; satisfies never keeps it exhaustive.
  • chat.ts and transport.ts drop ~50 lines each: tool registration is now tools: withFinalisationTool(LLM_STATIC_TOOLS).

Dead code pruned

  • bridge.createField + CreateFieldArgs removed (no LLM tool, no other consumer).
  • chat.toolInvocation.names.create_field i18n key dropped from all 23 locales.
  • createLlmFieldBaselineMiddleware + markFieldAsKnown plumbing removed (the create_field tool was never registered).

Iframe contract rename (also part of this PR)

  • REMOVE_FIELDSDELETE_FIELDS (response removed_countdeleted_count).
  • DELETE_PAGE { page }DELETE_PAGES { pages: number[] }.

React SDK (BREAKING)

  • actions.removeFieldsactions.deleteFields; removed_countdeleted_count. Major-version changeset added.

Net: +388 / -674 lines on copilot/. Adding a new bridge operation is now: schema in schemas.ts, method on IframeBridge, parseAndSend line in bridge.ts. Exposing it to the LLM: one entry in LLM_STATIC_TOOLS, one switch arm in factory.ts. The satisfies never exhaustiveness catches drift at compile time.

…(array)

Iframe contract:
- REMOVE_FIELDS -> DELETE_FIELDS; response field removed_count -> deleted_count
- DELETE_PAGE { page } -> DELETE_PAGES { pages: number[] } (non-empty)
  Validation: empty -> invalid_page; pages.length >= visible -> event_not_allowed
  (last-page guard upfront); per-element invalid_page / page_out_of_range.
  Visible-page positions resolved to absolute page numbers BEFORE deletion
  so multi-page batches stay consistent across mid-loop index shifts.

Copilot:
- Tool registry: remove_fields -> delete_fields, delete_page -> delete_pages
- Bridge: removeFields -> deleteFields, deletePage -> deletePages
- 23 locales: rename keys + plural-aware "Deleting pages" copy
- Drop dead `chat.toolInvocation.names.create_field` key (LLM never calls
  create_field; the matching createLlmFieldBaselineMiddleware was unreachable
  and is removed)
- System prompt updated to encourage batched delete_pages calls

React SDK (BREAKING):
- actions.removeFields -> actions.deleteFields
- RemoveFieldsResult.removed_count -> DeleteFieldsResult.deleted_count
- Internal postMessage type literal updated; changeset added (major bump)

embed/dev: panel rows updated; DELETE_PAGES accepts comma-separated input.
documentation/IFRAME.md: section + payload + response shape rewritten.
Drops dispatcher-side narrowing for LLM-driven bridge calls. The Zod tool
schemas validate shape at the AI SDK boundary, and the iframe handler in
client/lib/iframe/handlers.ts is the canonical runtime validator (it owns
range checks + visiblePageCount). A third validation layer in the
dispatcher just duplicates one of those and would drift over time.

- IframeBridge: goTo/setFieldValue/focusField/movePage/rotatePage/
  deletePages/deleteFields now accept `unknown` for LLM-supplied values.
  No non-dispatcher consumers exist for these methods.
- Dispatcher: 7 cases collapse to one-liners (set_field_value, focus_field,
  go_to_page, move_page, rotate_page, delete_pages, delete_fields).
- Header comment rewritten to call out the new contract.
Removes the bridge.createField method, CreateFieldArgs type, the
CREATE_FIELD BridgeRequestType variant, and the matching timeout-bucket
arm. The copilot demo never invoked it: create_field is not a registered
LLM tool (CLIENT_TOOL_NAMES omits it) and there are no direct
non-dispatcher consumers. Cleanup of dead surface — the iframe contract
itself still exposes CREATE_FIELD for SDK consumers, this only trims the
copilot's internal bridge.
safeDispatch's "unknown tool name" path is unreachable through the
standard Vercel AI SDK pathway: the SDK validates LLM tool calls against
the registered tool list before the dispatcher is invoked. Removing the
wrapper collapses the indirection; the factory now narrows toolName via
isClientToolName inline. The dispatcher's `default` arm + `satisfies never`
keeps the compile-time exhaustiveness over ClientToolName intact.
…me narrow

The factory's runtime isClientToolName check was redundant: the chat_pane.tsx
caller already narrows toolName via isClientToolName at the boundary
(rejecting unknowns with output-error before execute fires). Tightens
execute's signature to ClientToolName so the type system enforces what the
caller already guarantees, and the factory's middleware terminus collapses
to a one-line dispatch call. MiddlewareContext.toolName follows suit.

Net: one runtime narrow at the consumer boundary; everything downstream is
typed.
The bridge is the single source of truth for the iframe contract: each
operation has a Zod schema (with description) in
embed-bridge/schemas.ts, and IframeBridge methods take z.infer<typeof X>
directly. The bridge implementation is a one-line postMessage pass-
through per method — no key conversion (input shapes mirror the wire's
snake_case payloads).

The client-tools adapter is now just two files:
- schemas.ts: enumerates the LLM tool names exposed to the model.
- tools.ts: maps each tool name to { description, inputSchema } pulled
  verbatim from the bridge schema's `.describe()`. No duplicated text.

Consumers (routes/api/chat.ts and lib/byok/transport.ts) drop ~50 lines
of inline tool registration each; they now just spread LLM_STATIC_TOOLS
into withFinalisationTool. Adding a new LLM-exposed bridge operation:
- one schema in embed-bridge/schemas.ts
- one method on IframeBridge + one impl line in bridge.ts
- one tool entry in client-tools/tools.ts
- one switch arm in client-tools/factory.ts

dispatch.ts is gone (the switch lives in factory.ts directly).
The tool-name registry (CLIENT_TOOL_NAMES, ClientToolName,
isClientToolName) lived next to the LLM_STATIC_TOOLS map but in a
separate file for no real reason. Merging both into tools.ts puts every
LLM-tool decision (which schema, which name, the runtime guard) in one
place. Adding a tool now touches one file in the adapter (tools.ts) +
one switch arm in factory.ts.
Each bridge method now safeParses its `unknown` input via the matching
Zod schema before posting to the iframe. Bad input surfaces as
`{ success: false, error: { code: 'bad_input', message } }` without a
postMessage round-trip. The adapter (createClientTools switch terminus)
collapses to one-line cases that just hand the LLM input to the bridge.

- IframeBridge methods take `args: unknown` (uniform external surface).
- bridge.ts: parseAndSend helper centralises the parse + sendRequest pair.
- factory.ts: switch is pure routing, no schemas imported.
- sendRequest's `data` parameter loosened to `unknown` (it JSON.stringifies
  and doesn't care about the shape).

Adding a new bridge operation is now: schema in schemas.ts, method on
IframeBridge, parseAndSend line in bridge.ts, switch arm in factory.ts.
The schema is the single source of truth.
…comments

- Remove unused bridge.loadDocument + LoadDocumentInput + 'LOAD_DOCUMENT'
  from BridgeRequestType + matching arm in getRequestTimeoutMs. The
  copilot demo loads documents via URL params, never via postMessage —
  the method had no callers.
- Derive ClientToolName from `keyof typeof LLM_STATIC_TOOLS | 'submit' |
  'download'`. Drop the redundant CLIENT_TOOL_NAMES array. isClientToolName
  uses a derived ReadonlySet for the runtime guard. CLIENT_TOOL_NAMES is
  no longer exported from the barrel (it was internal only).
- Stale "LLM dispatcher" comment wording → "LLM tool registry" (dispatch.ts
  has been gone for a while).
…ool)

Restores `bridge.loadDocument` + `LoadDocumentInput`. The method stays
available for direct host-app consumers / SDK adapters; it is NOT in
LLM_STATIC_TOOLS or ClientToolName, so the LLM cannot invoke it via the
copilot tool registry. This separates the bridge surface (full iframe
contract) from the LLM-tool surface (a curated subset).
Bridge-owned codes (bad_input, bridge_disposed, iframe_not_ready,
missing_result, timeout) are now literal types; iframe-forwarded codes
(bad_request:*, forbidden:*, etc.) pass through via `string & {}` so
arbitrary strings still compile. The `& {}` idiom preserves IDE
autocomplete for the bridge-owned literals — typos like
`code: 'iframe_not_redy'` show up in suggestions; bridge-emitted code
is now self-documenting at the type level. Narrowing on a specific
iframe code stays the consumer's responsibility.

Exports BridgeErrorCode from the embed-bridge barrel so future
adapter / consumer code can reference the type if it ever needs to.
@bendersej bendersej merged commit 4b86b72 into main Apr 27, 2026
2 checks passed
@bendersej bendersej deleted the copilot-improvements branch April 27, 2026 16:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant