@skaile/forge-editor 6.2.1 → 6.4.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.
Files changed (57) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/dist/adapters/nitro.d.ts +11 -3
  3. package/dist/adapters/nitro.js +2 -1
  4. package/dist/adapters/nitro.js.map +1 -1
  5. package/dist/code/CodeSurface.vue.d.ts +1 -1
  6. package/dist/document/DocumentSurface.vue.d.ts +1 -1
  7. package/dist/features/ai/index.d.ts +2 -2
  8. package/dist/features/ai/index.js +2 -2
  9. package/dist/features/ai/protocol/chat.d.ts +5 -0
  10. package/dist/features/ai/protocol/chat.js +7 -4
  11. package/dist/features/ai/protocol/chat.js.map +1 -1
  12. package/dist/features/ai/protocol/index.d.ts +2 -2
  13. package/dist/features/ai/protocol/index.js +2 -2
  14. package/dist/features/composites/components/CompositeInspector.js +8 -8
  15. package/dist/features/composites/components/SettingsEditingTab.js +16 -3
  16. package/dist/features/composites/components/SettingsEditingTab.js.map +1 -1
  17. package/dist/features/composites/feature.js +26 -6
  18. package/dist/features/composites/feature.js.map +1 -1
  19. package/dist/features/composites/index.d.ts +2 -2
  20. package/dist/features/composites/index.js +2 -2
  21. package/dist/features/composites/react/SettingsEditingTab.js +22 -0
  22. package/dist/features/composites/react/SettingsEditingTab.js.map +1 -1
  23. package/dist/features/composites/split.d.ts +19 -1
  24. package/dist/features/composites/split.js +57 -4
  25. package/dist/features/composites/split.js.map +1 -1
  26. package/dist/features/composites/vue.js +1 -1
  27. package/dist/i18n/de/composites.js +4 -0
  28. package/dist/i18n/de/composites.js.map +1 -1
  29. package/dist/i18n/de.d.ts +4 -0
  30. package/dist/i18n/en/composites.js +4 -0
  31. package/dist/i18n/en/composites.js.map +1 -1
  32. package/dist/markdown/MarkdownSurface.vue.d.ts +1 -1
  33. package/dist/server/chat-agent.d.ts +85 -0
  34. package/dist/server/chat-agent.js +61 -0
  35. package/dist/server/chat-agent.js.map +1 -0
  36. package/dist/server/document-tools.d.ts +22 -0
  37. package/dist/server/document-tools.js +190 -0
  38. package/dist/server/document-tools.js.map +1 -0
  39. package/dist/server/handlers/ai.d.ts +2 -1
  40. package/dist/server/handlers/ai.js +232 -121
  41. package/dist/server/handlers/ai.js.map +1 -1
  42. package/dist/server/handlers/context.d.ts +4 -1
  43. package/dist/server/handlers/context.js.map +1 -1
  44. package/dist/server/handlers/editor.d.ts +1 -0
  45. package/dist/server/index.d.ts +7 -3
  46. package/dist/server/index.js +5 -2
  47. package/dist/server/workspace-tools.d.ts +1 -0
  48. package/dist/server/workspace-tools.js +8 -3
  49. package/dist/server/workspace-tools.js.map +1 -1
  50. package/dist/settings/index.d.ts +3 -0
  51. package/dist/settings/index.js +7 -3
  52. package/dist/settings/index.js.map +1 -1
  53. package/dist/tree/DocumentTree.js +1 -1
  54. package/dist/tree/use-tree-model.js +1 -1
  55. package/dist/vue/compose.js +1 -1
  56. package/dist/vue/index.js +1 -1
  57. package/package.json +1 -1
@@ -6,8 +6,8 @@ import { AI_ID_PATTERN, aiBlockSchema } from "../../features/ai/protocol/schemas
6
6
  import { generateBodySchema, generateProblem, runGenerate } from "../../features/ai/protocol/generate.js";
7
7
  import { AGENT_OP_OUTCOMES, AGENT_OP_TYPES } from "../../core/agent-op-types.js";
8
8
  import { readBlockLines } from "../../features/ai/protocol/block-lines.js";
9
- import { AGENT_LIMITS, AGENT_TOOL, agentOpTarget, agentRetryMessage, agentUnreadableProblem, checkAgentOps } from "../../features/ai/protocol/agent-ops.js";
10
- import { CHAT_LIMITS, documentChatMessages } from "../../features/ai/protocol/chat.js";
9
+ import { AGENT_LIMITS, AGENT_TOOL, agentOpTarget, agentRetryMessage, agentShownMessage, agentUnreadableProblem, checkAgentOps } from "../../features/ai/protocol/agent-ops.js";
10
+ import { CHAT_LIMITS, documentChatMessages, documentChatSystem } from "../../features/ai/protocol/chat.js";
11
11
  import { TranscriptCleanupUnreadableError, runTranscriptCleanup, transcriptCleanupRequestSchema } from "../../integrations/transcript-ai/protocol.js";
12
12
  import { blockExcerptText } from "../../core/block-snippet.js";
13
13
  import { AGENT_OP_ID } from "../../core/change-set.js";
@@ -16,9 +16,10 @@ import { aiBlockTypesOf } from "../../features/composites/block-types.js";
16
16
  import { ASSET_DOCUMENT_ID, ASSET_NAME } from "../../features/media/assets.js";
17
17
  import { loadCurrentContent } from "../stored-content.js";
18
18
  import { HttpError } from "../errors.js";
19
+ import { documentEditTool } from "../chat-agent.js";
19
20
  import { mayEditDocument } from "../readable-documents.js";
20
- import { toolRegistry } from "../tools.js";
21
21
  import { blockExcerptOf, blockLinesOfContent } from "../document-blocks.js";
22
+ import { toolRegistry } from "../tools.js";
22
23
  import { authenticated, enforceRateLimit, eventStream, json, port, readBody } from "./context.js";
23
24
  import { DOI_PATTERN } from "../crossref.js";
24
25
  import { MAX_TRANSCRIBE_BYTES, RasterError, bibliographyText, describeImage, extractBibliography, isScanned, joinPages, ocrPages, pdfPagesText, transcribe } from "../extract.js";
@@ -141,57 +142,108 @@ const chatBodySchema = z.object({
141
142
  structure: z.enum(STRUCTURE_MODES)
142
143
  });
143
144
  /**
144
- * The tools of one chat turn: `edit_document` (deliberately loose, so malformed calls reach `checkAgentOps` and get a precise
145
- * retry message) and, with `reads`, the registry's read-only tools. With `workspace` (the host keeps change sets and a mutator)
146
- * the operations of `edit_document` may name another Document by `documentId`.
145
+ * The tools of one chat turn: `edit_document` (`documentEditTool`, deliberately loose, so malformed calls reach `checkAgentOps`
146
+ * and get a precise retry message) and, with `reads`, the registry's read-only tools. With `workspace` (the host keeps change
147
+ * sets and a mutator) the operations of `edit_document` may name another Document by `documentId`.
147
148
  */
148
149
  function agentTools(registry, { workspace, reads }) {
150
+ const edit = documentEditTool({ workspace });
149
151
  return {
150
152
  [AGENT_TOOL]: tool({
151
- description: workspace ? "Schlägt Änderungen vor. Operationen am geöffneten Dokument zeigt die Nutzerin bzw. der Nutzer im Dokument zur Prüfung an; Operationen mit documentId (anderes Dokument des Arbeitsbereichs) landen in einem Änderungssatz, den die Nutzerin bzw. der Nutzer je Dokument prüft und übernimmt oder ablehnt." : "Schlägt Änderungen am Dokument vor. Jede Operation wird der Nutzerin bzw. dem Nutzer im Dokument zur Prüfung angezeigt.",
152
- inputSchema: jsonSchema({
153
- type: "object",
154
- properties: { ops: {
155
- type: "array",
156
- description: "Die Operationen in Dokumentreihenfolge.",
157
- items: {
158
- type: "object",
159
- properties: {
160
- type: {
161
- type: "string",
162
- enum: [...AGENT_OP_TYPES],
163
- description: "update: Blockinhalt ersetzen; delete: Block löschen; add: Blöcke einfügen; convert: Blocktyp ändern."
164
- },
165
- ...workspace && { documentId: {
166
- type: "string",
167
- description: "Nur für Blöcke eines anderen Dokuments: dessen ID (zusammen mit den Block-IDs aus find_blocks oder read_block). Es muss ein Dokument sein, das die Nutzerin bzw. der Nutzer bearbeiten darf. Ohne Angabe gilt das geöffnete Dokument."
168
- } },
169
- id: {
170
- type: "string",
171
- description: "update/delete/convert: ID des Blocks aus dem Dokument."
172
- },
173
- afterId: {
174
- type: ["string", "null"],
175
- description: "add (Pflicht): ID des Blocks, nach dem eingefügt wird; für den Dokumentanfang ausdrücklich null."
176
- },
177
- markdown: {
178
- type: "string",
179
- description: "update/add: der neue Inhalt als Markdown, ohne Block-ID."
180
- },
181
- to: {
182
- type: "string",
183
- description: "convert: der neue Blocktyp, einer der im Systemprompt erlaubten Werte (z. B. paragraph, heading2, composite:task)."
184
- }
185
- },
186
- required: ["type"]
187
- }
188
- } },
189
- required: ["ops"]
190
- })
153
+ description: edit.description,
154
+ inputSchema: jsonSchema(edit.inputSchema)
191
155
  }),
192
156
  ...reads && registry.tools
193
157
  };
194
158
  }
159
+ /** Checks a chat request beyond its schema (400 for an empty message or a selection without blocks of the document) and answers where its operations may point (`checkAgentOps`). */
160
+ function opContextOf({ messages, document, scope, targets }) {
161
+ if (!messages.at(-1).content.trim()) throw new HttpError(400, "Nachricht fehlt");
162
+ const lines = readBlockLines(document);
163
+ const allowed = scope ? new Set(scope.filter((id) => lines.ids.has(id))) : null;
164
+ if (allowed && !allowed.size) throw new HttpError(400, "Die Auswahl enthält keine Blöcke des Dokuments");
165
+ return {
166
+ lines,
167
+ allowed,
168
+ targets: new Set(targets)
169
+ };
170
+ }
171
+ /** Loads the Documents operations name, once per request (the model's read and its edit see the same content); `null` for one the user may not edit or that does not exist. */
172
+ function namedDocumentLoader(ctx, user, toolContext) {
173
+ const loaded = /* @__PURE__ */ new Map();
174
+ return (id) => {
175
+ let entry = loaded.get(id);
176
+ if (!entry) {
177
+ entry = (async () => {
178
+ if (!await mayEditDocument(ctx.ports, user, id)) return null;
179
+ const content = toolContext.liveContent(id) ?? await loadCurrentContent(ctx.ports.documents, id);
180
+ return content && {
181
+ content,
182
+ lines: blockLinesOfContent(content)
183
+ };
184
+ })();
185
+ loaded.set(id, entry);
186
+ }
187
+ return entry;
188
+ };
189
+ }
190
+ /** The lines of the other Documents `raw` names, as far as the user may edit them; empty without `documentOf` (no change sets: such an operation is a `document` problem). */
191
+ async function namedDocumentLines(raw, openDocumentId, documentOf) {
192
+ const documents = /* @__PURE__ */ new Map();
193
+ if (!documentOf) return documents;
194
+ const named = new Set(raw.flatMap((op) => typeof op === "object" && op !== null && "documentId" in op && typeof op.documentId === "string" ? [op.documentId.trim()] : []));
195
+ named.delete(openDocumentId ?? "");
196
+ for (const id of [...named].slice(0, AGENT_LIMITS.ops)) {
197
+ const entry = id ? await documentOf(id) : null;
198
+ if (entry) documents.set(id, entry.lines);
199
+ }
200
+ return documents;
201
+ }
202
+ /**
203
+ * Streams the operations of a turn, numbered from `op<nextOp>`: the open Document's as `ops` (the editor's in-document review),
204
+ * the others' as one stored `change-set`. Without an answer text and without a valid operation the user reads that none was possible.
205
+ */
206
+ async function emitProposal(emit, proposal) {
207
+ const { all, dropped, retries, answered, user, changeSets, documentOf } = proposal;
208
+ const ops = all.filter((op) => op.documentId === void 0);
209
+ const foreign = all.filter((op) => op.documentId !== void 0);
210
+ if (!answered && !all.length) emit("delta", JSON.stringify({ text: "Ich konnte keine gültigen Änderungen vorschlagen." }));
211
+ if (foreign.length) {
212
+ const hunks = await Promise.all(foreign.map(async (op) => {
213
+ const documentId = op.documentId;
214
+ const target = agentOpTarget(op);
215
+ const entry = await documentOf(documentId);
216
+ const excerpt = target === null || !entry ? null : blockExcerptOf(entry.content, target);
217
+ return {
218
+ op: {
219
+ ...op,
220
+ documentId
221
+ },
222
+ status: "pending",
223
+ target: excerpt && {
224
+ type: excerpt.type,
225
+ text: blockExcerptText(excerpt.text)
226
+ }
227
+ };
228
+ }));
229
+ emit("change-set", JSON.stringify(await changeSets.create({
230
+ by: changeSetOwner(user),
231
+ hunks
232
+ })));
233
+ }
234
+ emit("ops", JSON.stringify({
235
+ ops,
236
+ dropped,
237
+ retries
238
+ }));
239
+ }
240
+ /** `final` and the problems of earlier attempts whose block no operation of `all` targets: an operation a corrected list leaves out counts as dropped too. */
241
+ function droppedOf(all, final, earlier) {
242
+ const targets = new Set(all.map((op) => agentOpTarget(op)));
243
+ const dropped = [...final];
244
+ for (const problem of earlier) if (problem.id !== null && !targets.has(problem.id) && !dropped.some((d) => d.id === problem.id && d.type === problem.type)) dropped.push(problem);
245
+ return dropped;
246
+ }
195
247
  /**
196
248
  * Document chat: answers the last message of `messages` about `document`, or proposes edit operations through the
197
249
  * `edit_document` tool. Operations pointing to IDs that are unknown or outside `scope`, an `add` without `afterId`, an
@@ -203,24 +255,33 @@ function agentTools(registry, { workspace, reads }) {
203
255
  * Operations of the open Document (`documentId` absent or the body's) are `ops`; operations naming another Document the
204
256
  * user may edit (`mayEditDocument`) become one `ChangeSet` (`ChangeSetStore`) for the user to review per Document, when the
205
257
  * host has the `changeSets` port and a mutator; else such an operation is a `document` problem the model retries. All
206
- * operations share one numbering (`op<n>`).
258
+ * operations share one numbering (`op<n>`). With a host agent (`AiHandlerContext.chatAgent` answers one for the user) the
259
+ * agent answers the turn in place of the model loop (`agentChat`), with the same checks and events.
207
260
  */
208
261
  async function chatPost(ctx, request) {
209
262
  const user = await enforceRateLimit(ctx, request);
210
- const { messages, document, documentId: openDocumentId, scope, nextOp, model, blockTypes: clientBlockTypes, blocks, targets, structure } = await readBody(request, chatBodySchema);
211
- const blockTypes = ctx.blockTypes ? aiBlockTypesOf(ctx.blockTypes) : clientBlockTypes;
212
- const languageModel = await ctx.ai.resolveModel(model, user);
213
- if (!messages.at(-1).content.trim()) throw new HttpError(400, "Nachricht fehlt");
214
- const lines = readBlockLines(document);
215
- const allowed = scope ? new Set(scope.filter((id) => lines.ids.has(id))) : null;
216
- if (allowed && !allowed.size) throw new HttpError(400, "Die Auswahl enthält keine Blöcke des Dokuments");
217
- const context = {
218
- lines,
219
- allowed,
220
- targets: new Set(targets)
263
+ const body = await readBody(request, chatBodySchema);
264
+ const { messages, document, documentId: openDocumentId, nextOp, model, blockTypes: clientBlockTypes, blocks, targets, structure } = body;
265
+ const blockModel = {
266
+ blockTypes: ctx.blockTypes ? aiBlockTypesOf(ctx.blockTypes) : clientBlockTypes,
267
+ blocks,
268
+ targets,
269
+ structure
221
270
  };
222
271
  const registry = ctx.tools ?? NO_TOOLS;
223
272
  const changeSets = ctx.mutator ? ctx.ports.changeSets : void 0;
273
+ const agent = await ctx.chatAgent?.(user) ?? null;
274
+ if (agent) return agentChat(ctx, request, agent, {
275
+ user,
276
+ body,
277
+ blockModel,
278
+ context: opContextOf(body),
279
+ registry,
280
+ changeSets
281
+ });
282
+ const languageModel = await ctx.ai.resolveModel(model, user);
283
+ const context = opContextOf(body);
284
+ const { allowed } = context;
224
285
  const workspace = changeSets !== void 0;
225
286
  const editTools = agentTools(registry, {
226
287
  workspace,
@@ -232,12 +293,7 @@ async function chatPost(ctx, request) {
232
293
  });
233
294
  return eventStream(request, async (emit, signal) => {
234
295
  try {
235
- let conversation = documentChatMessages(messages, document, allowed && [...allowed], {
236
- blockTypes,
237
- blocks,
238
- targets,
239
- structure
240
- }, registry.infos);
296
+ let conversation = documentChatMessages(messages, document, allowed && [...allowed], blockModel, registry.infos);
241
297
  let answered = false;
242
298
  let result = null;
243
299
  const retried = [];
@@ -250,22 +306,7 @@ async function chatPost(ctx, request) {
250
306
  liveContent: (id) => ctx.collab?.liveContent(id) ?? null,
251
307
  ...openDocumentId && { documentId: openDocumentId }
252
308
  };
253
- const loaded = /* @__PURE__ */ new Map();
254
- const documentOf = (id) => {
255
- let entry = loaded.get(id);
256
- if (!entry) {
257
- entry = (async () => {
258
- if (!await mayEditDocument(ctx.ports, user, id)) return null;
259
- const content = toolContext.liveContent(id) ?? await loadCurrentContent(ctx.ports.documents, id);
260
- return content && {
261
- content,
262
- lines: blockLinesOfContent(content)
263
- };
264
- })();
265
- loaded.set(id, entry);
266
- }
267
- return entry;
268
- };
309
+ const documentOf = namedDocumentLoader(ctx, user, toolContext);
269
310
  for (;;) {
270
311
  const calls = [];
271
312
  const reads = [];
@@ -346,15 +387,7 @@ async function chatPost(ctx, request) {
346
387
  if (!calls.length) break;
347
388
  const unreadable = calls.some((call) => call.invalid || !Array.isArray(call.input?.ops));
348
389
  const raw = calls.flatMap((call) => call.input?.ops).filter((op) => op !== void 0);
349
- const documents = /* @__PURE__ */ new Map();
350
- if (changeSets) {
351
- const named = new Set(raw.flatMap((op) => typeof op?.documentId === "string" ? [op.documentId.trim()] : []));
352
- named.delete(openDocumentId ?? "");
353
- for (const id of [...named].slice(0, AGENT_LIMITS.ops)) {
354
- const entry = id ? await documentOf(id) : null;
355
- if (entry) documents.set(id, entry.lines);
356
- }
357
- }
390
+ const documents = await namedDocumentLines(raw, openDocumentId, changeSets ? documentOf : null);
358
391
  const checked = checkAgentOps(raw, {
359
392
  ...context,
360
393
  ...openDocumentId && { documentId: openDocumentId },
@@ -399,40 +432,118 @@ async function chatPost(ctx, request) {
399
432
  ...op,
400
433
  op: `op${nextOp + i}`
401
434
  }));
402
- const ops = all.filter((op) => op.documentId === void 0);
403
- const foreign = all.filter((op) => op.documentId !== void 0);
404
- const targets = new Set(all.map((op) => agentOpTarget(op)));
405
- const dropped = [...result.problems];
406
- for (const problem of retried) if (problem.id !== null && !targets.has(problem.id) && !dropped.some((d) => d.id === problem.id && d.type === problem.type)) dropped.push(problem);
407
- if (!answered && !all.length) emit("delta", JSON.stringify({ text: "Ich konnte keine gültigen Änderungen vorschlagen." }));
408
- if (foreign.length) {
409
- const hunks = await Promise.all(foreign.map(async (op) => {
410
- const documentId = op.documentId;
411
- const target = agentOpTarget(op);
412
- const entry = await documentOf(documentId);
413
- const excerpt = target === null || !entry ? null : blockExcerptOf(entry.content, target);
414
- return {
415
- op: {
416
- ...op,
417
- documentId
418
- },
419
- status: "pending",
420
- target: excerpt && {
421
- type: excerpt.type,
422
- text: blockExcerptText(excerpt.text)
423
- }
424
- };
425
- }));
426
- emit("change-set", JSON.stringify(await changeSets.create({
427
- by: changeSetOwner(user),
428
- hunks
429
- })));
430
- }
431
- emit("ops", JSON.stringify({
435
+ await emitProposal(emit, {
436
+ all,
437
+ dropped: droppedOf(all, result.problems, retried),
438
+ retries,
439
+ answered,
440
+ user,
441
+ changeSets,
442
+ documentOf
443
+ });
444
+ }
445
+ emit("done", "{}");
446
+ } catch (error) {
447
+ if (!signal.aborted) {
448
+ const { statusMessage, message } = error;
449
+ emit("error", JSON.stringify({ message: statusMessage ?? message ?? "Unbekannter Fehler" }));
450
+ }
451
+ }
452
+ });
453
+ }
454
+ /**
455
+ * The chat turn of a host's `ChatAgent`: the agent gets the request as `ChatAgentTurn` (snapshot, scope, block model, the
456
+ * read-only tools with their context, the built-in system prompt and `edit_document`) and streams text, the tool calls it ran
457
+ * and the operations `checkEdit` accepted. They become the built-in loop's events: `delta`, `tool`, then once the proposal of
458
+ * all edits (`change-set` for other Documents, `ops` numbered from `nextOp`), `done`; a thrown error is `error`. `checkEdit`
459
+ * checks each `edit_document` call against the operations of the earlier calls of the turn too (the limit, a block edited twice,
460
+ * an operation inside a block an earlier one replaces), and numbers on from them.
461
+ */
462
+ function agentChat(ctx, request, agent, chat) {
463
+ const { user, body: { messages, document, documentId: openDocumentId, nextOp }, blockModel, context, registry, changeSets } = chat;
464
+ return eventStream(request, async (emit, signal) => {
465
+ try {
466
+ const toolContext = {
467
+ ports: ctx.ports,
468
+ user,
469
+ signal,
470
+ liveContent: (id) => ctx.collab?.liveContent(id) ?? null,
471
+ ...openDocumentId && { documentId: openDocumentId }
472
+ };
473
+ const documentOf = namedDocumentLoader(ctx, user, toolContext);
474
+ const shown = [];
475
+ const checkEdit = async (input) => {
476
+ const value = typeof input === "object" && input !== null && "ops" in input ? input.ops : void 0;
477
+ const raw = [...shown, ...Array.isArray(value) ? value.filter((op) => op !== void 0) : []];
478
+ const documents = await namedDocumentLines(raw, openDocumentId, changeSets ? documentOf : null);
479
+ const checked = checkAgentOps(raw, {
480
+ ...context,
481
+ ...openDocumentId && { documentId: openDocumentId },
482
+ documents
483
+ });
484
+ const covered = checked.problems.filter((problem) => problem.index <= shown.length).length;
485
+ const fresh = checked.valid.slice(shown.length - covered);
486
+ const problems = checked.problems.filter((problem) => problem.index > shown.length).map((problem) => ({
487
+ ...problem,
488
+ index: problem.index - shown.length
489
+ }));
490
+ if (!Array.isArray(value)) problems.unshift(agentUnreadableProblem());
491
+ const ops = fresh.map((op, i) => ({
492
+ ...op,
493
+ op: `op${nextOp + shown.length + i}`
494
+ }));
495
+ shown.push(...fresh);
496
+ const message = agentShownMessage(ops, 0);
497
+ return {
432
498
  ops,
433
- dropped,
434
- retries
499
+ problems,
500
+ feedback: problems.length ? `${message}\n${agentRetryMessage(problems, !!context.allowed, { partial: true })}` : message
501
+ };
502
+ };
503
+ const proposed = [];
504
+ const attempts = [];
505
+ let answered = false;
506
+ for await (const part of agent.turn({
507
+ user,
508
+ ...openDocumentId && { documentId: openDocumentId },
509
+ messages,
510
+ document,
511
+ scope: context.allowed && [...context.allowed],
512
+ blockModel,
513
+ tools: registry,
514
+ toolContext,
515
+ system: documentChatSystem(blockModel, registry.infos),
516
+ editTool: documentEditTool({ workspace: changeSets !== void 0 }),
517
+ checkEdit,
518
+ signal
519
+ })) if (part.type === "text") {
520
+ if (!part.text) continue;
521
+ answered = true;
522
+ emit("delta", JSON.stringify({ text: part.text }));
523
+ } else if (part.type === "tool") emit("tool", JSON.stringify({
524
+ name: part.call.name,
525
+ input: toolInput(part.call.input),
526
+ output: part.call.output.slice(0, CHAT_LIMITS.toolOutput)
527
+ }));
528
+ else {
529
+ proposed.push(...part.ops);
530
+ attempts.push(part.dropped);
531
+ }
532
+ if (attempts.length) {
533
+ const all = proposed.map((op, i) => ({
534
+ ...op,
535
+ op: `op${nextOp + i}`
435
536
  }));
537
+ const retries = attempts.slice(1).filter((_, i) => attempts[i].length > 0).length;
538
+ await emitProposal(emit, {
539
+ all,
540
+ dropped: droppedOf(all, attempts.at(-1), attempts.slice(0, -1).flat()),
541
+ retries,
542
+ answered,
543
+ user,
544
+ changeSets,
545
+ documentOf
546
+ });
436
547
  }
437
548
  emit("done", "{}");
438
549
  } catch (error) {