Skip to content

Commit d9caf16

Browse files
committed
feat: rename REMOVE_FIELDS->DELETE_FIELDS, DELETE_PAGE->DELETE_PAGES (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.
1 parent 6a2ab0a commit d9caf16

41 files changed

Lines changed: 183 additions & 214 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
"@simplepdf/react-embed-pdf": major
3+
---
4+
5+
Renames `actions.removeFields` to `actions.deleteFields` and the corresponding iframe event from `REMOVE_FIELDS` to `DELETE_FIELDS`. The result payload field is renamed from `removed_count` to `deleted_count`. Aligns naming with the new `DELETE_PAGES` event so all destructive operations use `delete_*` consistently.
6+
7+
If you are not using `actions.removeFields(...)` or `sendEvent("REMOVE_FIELDS", ...)`, you can safely update to this new major version.
8+
9+
```ts
10+
// Before
11+
const result = await actions.removeFields({ page: 1 });
12+
if (result.success) {
13+
console.log(result.data.removed_count);
14+
}
15+
16+
// After
17+
const result = await actions.deleteFields({ page: 1 });
18+
if (result.success) {
19+
console.log(result.data.deleted_count);
20+
}
21+
```

copilot/src/components/chat/chat_pane.tsx

Lines changed: 6 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -324,25 +324,6 @@ const createToolbarSyncMiddleware =
324324
return result
325325
}
326326

327-
// When the LLM itself creates a field (via `create_field`), the iframe's
328-
// field set grows by one. If we did nothing, the post-stream getFields
329-
// would diff that field as "user-added" and nudge the LLM about a field
330-
// it just created itself. This middleware extracts the new field id from
331-
// the bridge result and forwards it to the host so the field-detection
332-
// hook can pre-mark it as known.
333-
const createLlmFieldBaselineMiddleware =
334-
({ onLlmCreatedField }: { onLlmCreatedField: (fieldId: string) => void }): ToolMiddleware =>
335-
async ({ toolName }, next) => {
336-
const result = await next()
337-
if (toolName === 'create_field' && result.success) {
338-
const data = result.data
339-
if (data !== null && typeof data === 'object' && 'field_id' in data && typeof data.field_id === 'string') {
340-
onLlmCreatedField(data.field_id)
341-
}
342-
}
343-
return result
344-
}
345-
346327
const toUnexpectedToolResult = (error: unknown): BridgeResult<null> => {
347328
const errorMessage = error instanceof Error ? `${error.name}: ${error.message}` : String(error)
348329
return {
@@ -640,13 +621,13 @@ export const ChatPane = ({
640621
}, [])
641622

642623
// Refs-not-props for isStreaming + onFieldAdded: useDetectUserAddedField
643-
// must be called BEFORE `tools` useMemo (which needs markFieldAsKnown),
644-
// but both of those pieces of information come from useChat which runs
645-
// AFTER `tools`. Refs break the cycle; they are synced once useChat's
646-
// output is in scope (a bit further down in this component).
624+
// must be called BEFORE `tools` useMemo, but both of those pieces of
625+
// information come from useChat which runs AFTER `tools`. Refs break the
626+
// cycle; they are synced once useChat's output is in scope (a bit further
627+
// down in this component).
647628
const isStreamingRef = useRef(false)
648629
const onFieldAddedRef = useRef<(event: { tools: SupportedFieldType[]; delta: number }) => void>(() => {})
649-
const { markFieldAsKnown: markFieldDetectionAsKnown } = useDetectUserAddedField({
630+
useDetectUserAddedField({
650631
bridge,
651632
isReady,
652633
toolbarTool,
@@ -665,11 +646,6 @@ export const ChatPane = ({
665646
}
666647
const sharedMiddleware: ToolMiddleware[] = [
667648
createToolbarSyncMiddleware({ onChange: setToolbarTool }),
668-
createLlmFieldBaselineMiddleware({
669-
// When the LLM creates a field, mark its id as known so the next
670-
// user-placed-field diff does not attribute it to the user.
671-
onLlmCreatedField: (fieldId) => markFieldDetectionAsKnown(fieldId),
672-
}),
673649
createCompactionMiddleware({ getByokActive: () => byokConfigRef.current !== null }),
674650
]
675651
// Demo-only middleware lives at the head of the chain so it
@@ -684,7 +660,7 @@ export const ChatPane = ({
684660
systemPrompt: SYSTEM_PROMPT,
685661
middleware,
686662
})
687-
}, [bridge, handleDownloadRequested, markFieldDetectionAsKnown])
663+
}, [bridge, handleDownloadRequested])
688664

689665
const { messages, status, error, sendMessage, stop, addToolOutput, setMessages } = useChat({
690666
transport,

copilot/src/components/chat/hooks/use_detect_user_added_field.ts

Lines changed: 2 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { type MutableRefObject, useCallback, useEffect, useRef } from 'react'
1+
import { type MutableRefObject, useEffect, useRef } from 'react'
22
import type { IframeBridge, SupportedFieldType } from '../../../lib/embed-bridge'
33

44
// WORKAROUND: the SimplePDF editor does not currently emit an outbound
@@ -30,11 +30,6 @@ import type { IframeBridge, SupportedFieldType } from '../../../lib/embed-bridge
3030
// the UI can show one icon per unique type when the user mixed (e.g.
3131
// TEXT + SIGNATURE in the same batch).
3232
//
33-
// LLM-created fields bypass this nudge via `markFieldAsKnown(fieldId)`,
34-
// called from the create_field middleware once the iframe has confirmed
35-
// the new field id. The id goes straight into the seen set; the next
36-
// poll's diff sees no user-added fields.
37-
//
3833
// Refs-not-props for the streaming flag and the fire callback let the
3934
// hook be called BEFORE useChat in the consumer (useChat produces the
4035
// status + sendMessage used downstream). The consumer syncs the refs
@@ -53,31 +48,17 @@ type UseDetectUserAddedFieldArgs = {
5348
onFieldAddedRef: MutableRefObject<(event: FieldAddedEvent) => void>
5449
}
5550

56-
type UseDetectUserAddedFieldReturn = {
57-
// Consumers call this when they know a field was added by something
58-
// other than the user (e.g. the LLM's `create_field` tool returned a
59-
// field id). The id is added to the seen set so the next poll does
60-
// NOT attribute that field to the user.
61-
markFieldAsKnown: (fieldId: string) => void
62-
}
63-
6451
export const useDetectUserAddedField = ({
6552
bridge,
6653
isReady,
6754
toolbarTool,
6855
isCursorOverEditor,
6956
isStreamingRef,
7057
onFieldAddedRef,
71-
}: UseDetectUserAddedFieldArgs): UseDetectUserAddedFieldReturn => {
58+
}: UseDetectUserAddedFieldArgs): void => {
7259
const seenIdsRef = useRef<Set<string> | null>(null)
7360
const lastBridgeRef = useRef<IframeBridge | null>(null)
7461

75-
const markFieldAsKnown = useCallback((fieldId: string): void => {
76-
if (seenIdsRef.current !== null) {
77-
seenIdsRef.current.add(fieldId)
78-
}
79-
}, [])
80-
8162
useEffect(() => {
8263
// Bridge swap is the only event that invalidates the seen set; the
8364
// ids belong to a different document context. Tool changes, cursor
@@ -145,6 +126,4 @@ export const useDetectUserAddedField = ({
145126
}
146127
}
147128
}, [bridge, isReady, toolbarTool, isCursorOverEditor, isStreamingRef, onFieldAddedRef])
148-
149-
return { markFieldAsKnown }
150129
}

copilot/src/lib/byok/transport.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { convertToModelMessages, streamText, type UIMessage } from 'ai'
22
import { buildSystemPrompt } from '../../server/tools'
33
import {
4-
DeletePageInput,
4+
DeleteFieldsInput,
5+
DeletePagesInput,
56
DetectFieldsInput,
67
FINALISATION_ACTION,
78
FocusFieldInput,
89
GetDocumentContentInput,
910
GetFieldsInput,
1011
GoToPageInput,
1112
MovePageInput,
12-
RemoveFieldsInput,
1313
RotatePageInput,
1414
SelectToolInput,
1515
SetFieldValueInput,
@@ -107,10 +107,10 @@ export const runByokStream = async ({ config, init }: RunByokStreamArgs): Promis
107107
'Asks the editor to auto-detect and create missing fields. Call this when get_fields returned 0 fields.',
108108
inputSchema: DetectFieldsInput,
109109
},
110-
remove_fields: {
110+
delete_fields: {
111111
description:
112-
'Removes fields from the document. field_ids targets specific fields by id; page targets a single page (1-indexed); both omitted clears all fields. Destructive — only call when the user explicitly asks to remove fields.',
113-
inputSchema: RemoveFieldsInput,
112+
'Deletes fields from the document. field_ids targets specific fields by id; page targets a single page (1-indexed); both omitted clears all fields. Destructive — only call when the user explicitly asks to delete fields.',
113+
inputSchema: DeleteFieldsInput,
114114
},
115115
select_tool: {
116116
description:
@@ -134,10 +134,10 @@ export const runByokStream = async ({ config, init }: RunByokStreamArgs): Promis
134134
'Reorders pages: from_page and to_page are 1-indexed visible page positions. Destructive — only call when the user explicitly asks to reorder a page.',
135135
inputSchema: MovePageInput,
136136
},
137-
delete_page: {
137+
delete_pages: {
138138
description:
139-
'Permanently removes a visible page (1-indexed) and any fields placed on it. The last remaining page cannot be deleted. Destructive — only call when the user explicitly asks to delete a page.',
140-
inputSchema: DeletePageInput,
139+
'Permanently removes one or more visible pages (1-indexed) and any fields placed on them. Pass pages as a non-empty array. At least one visible page must remain — passing every visible page returns event_not_allowed. Destructive — only call when the user explicitly asks to delete pages.',
140+
inputSchema: DeletePagesInput,
141141
},
142142
rotate_page: {
143143
description:

copilot/src/lib/embed-bridge-adapters/client-tools/dispatch.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const dispatch = async (
3131
}
3232
case 'detect_fields':
3333
return bridge.detectFields()
34-
case 'remove_fields': {
34+
case 'delete_fields': {
3535
const rawIds = input.field_ids
3636
const fieldIds = ((): string[] | null | 'invalid' => {
3737
if (rawIds === undefined || rawIds === null) {
@@ -49,7 +49,7 @@ export const dispatch = async (
4949
}
5050
}
5151
const page = typeof input.page === 'number' ? input.page : null
52-
return bridge.removeFields({ fieldIds, page })
52+
return bridge.deleteFields({ fieldIds, page })
5353
}
5454
case 'select_tool': {
5555
const rawTool = input.tool
@@ -104,15 +104,19 @@ export const dispatch = async (
104104
}
105105
return bridge.movePage({ fromPage, toPage })
106106
}
107-
case 'delete_page': {
108-
const page = typeof input.page === 'number' ? input.page : null
109-
if (page === null) {
107+
case 'delete_pages': {
108+
const rawPages = input.pages
109+
if (
110+
!Array.isArray(rawPages) ||
111+
rawPages.length === 0 ||
112+
!rawPages.every((page): page is number => typeof page === 'number' && Number.isInteger(page) && page > 0)
113+
) {
110114
return {
111115
success: false,
112-
error: { code: 'bad_input', message: 'page must be a number' },
116+
error: { code: 'bad_input', message: 'pages must be a non-empty array of positive integers' },
113117
}
114118
}
115-
return bridge.deletePage({ page })
119+
return bridge.deletePages({ pages: rawPages })
116120
}
117121
case 'rotate_page': {
118122
const page = typeof input.page === 'number' ? input.page : null

copilot/src/lib/embed-bridge-adapters/client-tools/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ export { composeMiddleware } from './middleware'
99
export type { ClientToolName } from './schemas'
1010
export {
1111
CLIENT_TOOL_SCHEMAS,
12-
DeletePageInput,
12+
DeleteFieldsInput,
13+
DeletePagesInput,
1314
DetectFieldsInput,
1415
DownloadInput,
1516
FocusFieldInput,
@@ -18,7 +19,6 @@ export {
1819
GoToPageInput,
1920
isClientToolName,
2021
MovePageInput,
21-
RemoveFieldsInput,
2222
RotatePageInput,
2323
SelectToolInput,
2424
SetFieldValueInput,

copilot/src/lib/embed-bridge-adapters/client-tools/schemas.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ export const DetectFieldsInput = z
1818
'Asks the editor to auto-detect and create fields on the document. Use when get_fields returned 0 fields before asking the user to add fields manually.',
1919
)
2020

21-
export const RemoveFieldsInput = z
21+
export const DeleteFieldsInput = z
2222
.object({
23-
field_ids: z.array(z.string()).optional().describe('Specific field identifiers to remove (omit to target by page or all)'),
23+
field_ids: z.array(z.string()).optional().describe('Specific field identifiers to delete (omit to target by page or all)'),
2424
page: z.number().int().positive().optional().describe('1-indexed visible page to clear (omit to target specific ids or all)'),
2525
})
2626
.describe(
27-
'Removes fields from the document. Pass field_ids to remove specific fields, page to clear a single page, or both omitted to remove every field. Destructive: only call when the user explicitly asks.',
27+
'Deletes fields from the document. Pass field_ids to delete specific fields, page to clear a single page, or both omitted to delete every field. Destructive: only call when the user explicitly asks.',
2828
)
2929

3030
// Aligned with the bridge's SupportedFieldType. The LLM may pick any of
@@ -70,10 +70,15 @@ export const MovePageInput = z
7070
'Reorders pages in the document. Destructive: only call when the user explicitly asks to reorder a page.',
7171
)
7272

73-
export const DeletePageInput = z
74-
.object({ page: z.number().int().positive().describe('Visible page to delete (1-indexed)') })
73+
export const DeletePagesInput = z
74+
.object({
75+
pages: z
76+
.array(z.number().int().positive())
77+
.nonempty()
78+
.describe('Visible pages to delete (1-indexed). Must be a non-empty array.'),
79+
})
7580
.describe(
76-
'Permanently removes a page (and any fields on it) from the document. Destructive: only call when the user explicitly asks to delete a page. The last remaining visible page cannot be deleted.',
81+
'Permanently removes one or more pages (and any fields on them) from the document. Destructive: only call when the user explicitly asks to delete pages. At least one visible page must remain — passing every visible page returns event_not_allowed.',
7782
)
7883

7984
export const RotatePageInput = z
@@ -98,13 +103,13 @@ export const CLIENT_TOOL_NAMES = [
98103
'get_fields',
99104
'get_document_content',
100105
'detect_fields',
101-
'remove_fields',
106+
'delete_fields',
102107
'select_tool',
103108
'set_field_value',
104109
'focus_field',
105110
'go_to_page',
106111
'move_page',
107-
'delete_page',
112+
'delete_pages',
108113
'rotate_page',
109114
'submit',
110115
'download',
@@ -122,13 +127,13 @@ export const CLIENT_TOOL_SCHEMAS = {
122127
get_fields: GetFieldsInput,
123128
get_document_content: GetDocumentContentInput,
124129
detect_fields: DetectFieldsInput,
125-
remove_fields: RemoveFieldsInput,
130+
delete_fields: DeleteFieldsInput,
126131
select_tool: SelectToolInput,
127132
set_field_value: SetFieldValueInput,
128133
focus_field: FocusFieldInput,
129134
go_to_page: GoToPageInput,
130135
move_page: MovePageInput,
131-
delete_page: DeletePageInput,
136+
delete_pages: DeletePagesInput,
132137
rotate_page: RotatePageInput,
133138
submit: SubmitInput,
134139
download: DownloadInput,

copilot/src/lib/embed-bridge/bridge.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
type IframeBridge,
1010
isBridgeResultLike,
1111
type LoadDocumentArgs,
12-
type RemoveFieldsArgs,
12+
type DeleteFieldsArgs,
1313
} from './types'
1414

1515
type PendingRequest = {
@@ -35,14 +35,14 @@ const getRequestTimeoutMs = (requestType: BridgeRequestType): number => {
3535
case 'GET_DOCUMENT_CONTENT':
3636
return HEAVY_REQUEST_TIMEOUT_MS
3737
case 'CREATE_FIELD':
38-
case 'DELETE_PAGE':
38+
case 'DELETE_FIELDS':
39+
case 'DELETE_PAGES':
3940
case 'FOCUS_FIELD':
4041
case 'GET_FIELDS':
4142
case 'DOWNLOAD':
4243
case 'GO_TO':
4344
case 'LOAD_DOCUMENT':
4445
case 'MOVE_PAGE':
45-
case 'REMOVE_FIELDS':
4646
case 'ROTATE_PAGE':
4747
case 'SELECT_TOOL':
4848
case 'SET_FIELD_VALUE':
@@ -379,8 +379,8 @@ export const createBridge = ({
379379
goTo: ({ page }) => sendRequest('GO_TO', { page }),
380380
selectTool: ({ tool }) => sendRequest('SELECT_TOOL', { tool }),
381381
detectFields: (args) => sendRequest('DETECT_FIELDS', { debug_mode: args?.debugMode === true }),
382-
removeFields: (args?: RemoveFieldsArgs) =>
383-
sendRequest('REMOVE_FIELDS', {
382+
deleteFields: (args?: DeleteFieldsArgs) =>
383+
sendRequest('DELETE_FIELDS', {
384384
field_ids: args?.fieldIds ?? null,
385385
page: args?.page ?? null,
386386
}),
@@ -402,7 +402,7 @@ export const createBridge = ({
402402
submit: ({ downloadCopy }) => sendRequest('SUBMIT', { download_copy: downloadCopy }),
403403
download: () => sendRequest('DOWNLOAD', {}),
404404
movePage: ({ fromPage, toPage }) => sendRequest('MOVE_PAGE', { from_page: fromPage, to_page: toPage }),
405-
deletePage: ({ page }) => sendRequest('DELETE_PAGE', { page }),
405+
deletePages: ({ pages }) => sendRequest('DELETE_PAGES', { pages }),
406406
rotatePage: ({ page }) => sendRequest('ROTATE_PAGE', { page }),
407407
}
408408

copilot/src/lib/embed-bridge/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ export type {
77
BridgeResult,
88
BridgeState,
99
CreateFieldArgs,
10+
DeleteFieldsArgs,
1011
DocumentContentPage,
1112
DocumentContentResult,
1213
FieldRecord,
1314
IframeBridge,
1415
LoadDocumentArgs,
15-
RemoveFieldsArgs,
1616
SupportedFieldType,
1717
} from './types'
1818
export { isBridgeResultLike } from './types'

0 commit comments

Comments
 (0)