@ecsplico/editor 2.0.0 → 2.1.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 (278) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +4 -3
  3. package/dist/adapters/anchors.d.ts +80 -0
  4. package/dist/adapters/anchors.js +270 -0
  5. package/dist/adapters/anchors.js.map +1 -0
  6. package/dist/adapters/comments/client.d.ts +64 -0
  7. package/dist/adapters/comments/client.js +536 -0
  8. package/dist/adapters/comments/client.js.map +1 -0
  9. package/dist/adapters/comments/drizzle.d.ts +430 -0
  10. package/dist/adapters/comments/drizzle.js +236 -0
  11. package/dist/adapters/comments/drizzle.js.map +1 -0
  12. package/dist/adapters/comments/errors.d.ts +9 -0
  13. package/dist/adapters/comments/errors.js +19 -0
  14. package/dist/adapters/comments/errors.js.map +1 -0
  15. package/dist/adapters/comments/json.d.ts +19 -0
  16. package/dist/adapters/comments/json.js +94 -0
  17. package/dist/adapters/comments/json.js.map +1 -0
  18. package/dist/adapters/comments/record.js +78 -0
  19. package/dist/adapters/comments/record.js.map +1 -0
  20. package/dist/adapters/comments/server.d.ts +23 -0
  21. package/dist/adapters/comments/server.js +102 -0
  22. package/dist/adapters/comments/server.js.map +1 -0
  23. package/dist/adapters/comments/types.d.ts +109 -0
  24. package/dist/adapters/documents.js +108 -0
  25. package/dist/adapters/documents.js.map +1 -0
  26. package/dist/adapters/drizzle.d.ts +293 -0
  27. package/dist/adapters/drizzle.js +157 -0
  28. package/dist/adapters/drizzle.js.map +1 -0
  29. package/dist/adapters/files.js +30 -0
  30. package/dist/adapters/files.js.map +1 -0
  31. package/dist/adapters/forge.d.ts +58 -0
  32. package/dist/adapters/forge.js +91 -0
  33. package/dist/adapters/forge.js.map +1 -0
  34. package/dist/adapters/index.d.ts +13 -0
  35. package/dist/adapters/index.js +0 -0
  36. package/dist/adapters/json.d.ts +24 -0
  37. package/dist/adapters/json.js +128 -0
  38. package/dist/adapters/json.js.map +1 -0
  39. package/dist/adapters/nitro.d.ts +61 -0
  40. package/dist/adapters/nitro.js +212 -0
  41. package/dist/adapters/nitro.js.map +1 -0
  42. package/dist/adapters/routes.js +16 -0
  43. package/dist/adapters/routes.js.map +1 -0
  44. package/dist/adapters/shared.d.ts +37 -0
  45. package/dist/adapters/shared.js +32 -0
  46. package/dist/adapters/shared.js.map +1 -0
  47. package/dist/adapters/vue.d.ts +67 -0
  48. package/dist/adapters/vue.js +187 -0
  49. package/dist/adapters/vue.js.map +1 -0
  50. package/dist/code/CodeSurface.vue.d.ts +3 -3
  51. package/dist/core/doc-anchors.d.ts +5 -0
  52. package/dist/core/doc-anchors.js +4 -3
  53. package/dist/core/doc-anchors.js.map +1 -1
  54. package/dist/core/editor-ui.js +57 -1
  55. package/dist/core/editor-ui.js.map +1 -1
  56. package/dist/core/surface-session.js +139 -0
  57. package/dist/core/surface-session.js.map +1 -0
  58. package/dist/document/DocumentSurface.d.ts +1 -1
  59. package/dist/document/DocumentSurface.vue.d.ts +3 -3
  60. package/dist/features/ai/agent-op-review.d.ts +2 -2
  61. package/dist/features/ai/agent-op-review.js.map +1 -1
  62. package/dist/features/ai/agent-ops.d.ts +1 -1
  63. package/dist/features/ai/agent-ops.js +9 -6
  64. package/dist/features/ai/agent-ops.js.map +1 -1
  65. package/dist/features/ai/ai-apply.d.ts +1 -1
  66. package/dist/features/ai/ai-review.js +10 -1
  67. package/dist/features/ai/ai-review.js.map +1 -1
  68. package/dist/features/ai/components/AiSelectionMenu.js +78 -252
  69. package/dist/features/ai/components/AiSelectionMenu.js.map +1 -1
  70. package/dist/features/ai/components/AiSelectionMenu.vue.d.ts +2 -1
  71. package/dist/features/ai/components/AiSidebar.js +2 -2
  72. package/dist/features/ai/components/DocumentChatAnswer.js +1 -1
  73. package/dist/features/ai/components/DocumentChatOps.js +1 -0
  74. package/dist/features/ai/components/DocumentChatOps.js.map +1 -1
  75. package/dist/features/ai/components/ModelQuickSwitch.js +1 -1
  76. package/dist/features/ai/components/SettingsAiTab.js +1 -1
  77. package/dist/features/ai/components/SettingsModelProfiles.js +1 -1
  78. package/dist/features/ai/document-chat.d.ts +3 -2
  79. package/dist/features/ai/document-chat.js +16 -19
  80. package/dist/features/ai/document-chat.js.map +1 -1
  81. package/dist/features/ai/index.d.ts +5 -3
  82. package/dist/features/ai/index.js +4 -2
  83. package/dist/features/ai/protocol/agent-ops.d.ts +22 -17
  84. package/dist/features/ai/protocol/agent-ops.js +65 -39
  85. package/dist/features/ai/protocol/agent-ops.js.map +1 -1
  86. package/dist/features/ai/protocol/block-lines.d.ts +89 -0
  87. package/dist/features/ai/protocol/block-lines.js +177 -0
  88. package/dist/features/ai/protocol/block-lines.js.map +1 -0
  89. package/dist/features/ai/protocol/chat.js +10 -10
  90. package/dist/features/ai/protocol/chat.js.map +1 -1
  91. package/dist/features/ai/protocol/index.d.ts +3 -2
  92. package/dist/features/ai/protocol/index.js +3 -2
  93. package/dist/features/ai/react/AiSelectionMenu.d.ts +3 -2
  94. package/dist/features/ai/react/AiSelectionMenu.js +64 -255
  95. package/dist/features/ai/react/AiSelectionMenu.js.map +1 -1
  96. package/dist/features/ai/react/AiSidebar.js +2 -2
  97. package/dist/features/ai/react/DocumentChatAnswer.js +1 -1
  98. package/dist/features/ai/react/DocumentChatOps.js +1 -0
  99. package/dist/features/ai/react/DocumentChatOps.js.map +1 -1
  100. package/dist/features/ai/react/ModelQuickSwitch.js +1 -1
  101. package/dist/features/ai/react/SettingsAiTab.js +1 -1
  102. package/dist/features/ai/react/SettingsModelProfiles.js +1 -1
  103. package/dist/features/ai/react/use-ai-selection-flow.js +34 -0
  104. package/dist/features/ai/react/use-ai-selection-flow.js.map +1 -0
  105. package/dist/features/ai/selection-flow.d.ts +133 -0
  106. package/dist/features/ai/selection-flow.js +283 -0
  107. package/dist/features/ai/selection-flow.js.map +1 -0
  108. package/dist/features/ai/use-ai-assistant.d.ts +1 -1
  109. package/dist/features/ai/use-document-chat.d.ts +1 -1
  110. package/dist/features/authors/components/AuthorColorsPanel.js +1 -1
  111. package/dist/features/authors/feature.js +1 -1
  112. package/dist/features/authors/index.js +1 -1
  113. package/dist/features/authors/react/AuthorColorsPanel.js +1 -1
  114. package/dist/features/comments/components/CommentBubble.js +1 -1
  115. package/dist/features/comments/components/CommentComposer.js +1 -1
  116. package/dist/features/comments/components/CommentThreadView.js +5 -2
  117. package/dist/features/comments/components/CommentThreadView.js.map +1 -1
  118. package/dist/features/comments/components/CommentsMargin.js +108 -230
  119. package/dist/features/comments/components/CommentsMargin.js.map +1 -1
  120. package/dist/features/comments/components/CommentsOverlay.js +1 -1
  121. package/dist/features/comments/feature.js +1 -1
  122. package/dist/features/comments/index.d.ts +4 -2
  123. package/dist/features/comments/index.js +4 -2
  124. package/dist/features/comments/margin-controller.d.ts +44 -0
  125. package/dist/features/comments/margin-controller.js +132 -0
  126. package/dist/features/comments/margin-controller.js.map +1 -0
  127. package/dist/features/comments/margin-layout.d.ts +55 -0
  128. package/dist/features/comments/margin-layout.js +79 -0
  129. package/dist/features/comments/margin-layout.js.map +1 -0
  130. package/dist/features/comments/model.d.ts +8 -0
  131. package/dist/features/comments/model.js +60 -34
  132. package/dist/features/comments/model.js.map +1 -1
  133. package/dist/features/comments/react/CommentBubble.js +1 -1
  134. package/dist/features/comments/react/CommentComposer.js +1 -1
  135. package/dist/features/comments/react/CommentThreadView.js +2 -2
  136. package/dist/features/comments/react/CommentThreadView.js.map +1 -1
  137. package/dist/features/comments/react/CommentsMargin.d.ts +0 -5
  138. package/dist/features/comments/react/CommentsMargin.js +85 -251
  139. package/dist/features/comments/react/CommentsMargin.js.map +1 -1
  140. package/dist/features/comments/react/CommentsOverlay.js +1 -1
  141. package/dist/features/comments/react/use-comments.js +1 -1
  142. package/dist/features/comments/react/use-margin-layout.js +45 -0
  143. package/dist/features/comments/react/use-margin-layout.js.map +1 -0
  144. package/dist/features/comments/react.d.ts +1 -1
  145. package/dist/features/comments/schema.d.ts +23 -0
  146. package/dist/features/comments/schema.js.map +1 -1
  147. package/dist/features/comments/snapshot-equality.js +40 -0
  148. package/dist/features/comments/snapshot-equality.js.map +1 -0
  149. package/dist/features/comments/thread-anchor.js +33 -0
  150. package/dist/features/comments/thread-anchor.js.map +1 -0
  151. package/dist/features/comments/use-comments.d.ts +3 -1
  152. package/dist/features/comments/use-comments.js +1 -0
  153. package/dist/features/comments/use-comments.js.map +1 -1
  154. package/dist/features/comments/use-margin-layout.js +29 -0
  155. package/dist/features/comments/use-margin-layout.js.map +1 -0
  156. package/dist/features/comments/vue.d.ts +1 -1
  157. package/dist/features/composites/accepted-children.d.ts +21 -0
  158. package/dist/features/composites/accepted-children.js +126 -0
  159. package/dist/features/composites/accepted-children.js.map +1 -0
  160. package/dist/features/composites/components/CompositeDetails.js +1 -1
  161. package/dist/features/composites/components/CompositeInspector.js +38 -164
  162. package/dist/features/composites/components/CompositeInspector.js.map +1 -1
  163. package/dist/features/composites/feature.d.ts +2 -1
  164. package/dist/features/composites/feature.js +8 -2
  165. package/dist/features/composites/feature.js.map +1 -1
  166. package/dist/features/composites/index.d.ts +5 -3
  167. package/dist/features/composites/index.js +8 -6
  168. package/dist/features/composites/inspector-model.d.ts +97 -0
  169. package/dist/features/composites/inspector-model.js +255 -0
  170. package/dist/features/composites/inspector-model.js.map +1 -0
  171. package/dist/features/composites/kinds.d.ts +11 -0
  172. package/dist/features/composites/kinds.js +17 -2
  173. package/dist/features/composites/kinds.js.map +1 -1
  174. package/dist/features/composites/outline.d.ts +18 -5
  175. package/dist/features/composites/outline.js +44 -12
  176. package/dist/features/composites/outline.js.map +1 -1
  177. package/dist/features/composites/react/CompositeDetails.js +1 -1
  178. package/dist/features/composites/react/CompositeInspector.d.ts +1 -1
  179. package/dist/features/composites/react/CompositeInspector.js +21 -180
  180. package/dist/features/composites/react/CompositeInspector.js.map +1 -1
  181. package/dist/features/composites/react/CompositeStatusMenu.d.ts +1 -1
  182. package/dist/features/composites/react/CompositeView.js +1 -1
  183. package/dist/features/composites/react/use-composite-inspector.js +29 -0
  184. package/dist/features/composites/react/use-composite-inspector.js.map +1 -0
  185. package/dist/features/composites/schema.d.ts +2 -2
  186. package/dist/features/composites/schema.js +2 -2
  187. package/dist/features/composites/schema.js.map +1 -1
  188. package/dist/features/composites/use-composite-anchors.d.ts +1 -1
  189. package/dist/features/composites/use-composite-inspector.js +41 -0
  190. package/dist/features/composites/use-composite-inspector.js.map +1 -0
  191. package/dist/features/composites/use-composite-rows.d.ts +1 -1
  192. package/dist/features/media/index.d.ts +1 -1
  193. package/dist/features/media/vue.d.ts +1 -1
  194. package/dist/features/mentions/index.d.ts +3 -3
  195. package/dist/features/mentions/index.js +3 -3
  196. package/dist/features/mentions/schema.d.ts +1 -1
  197. package/dist/features/mentions/schema.js +1 -1
  198. package/dist/features/themes/index.d.ts +1 -1
  199. package/dist/features/themes/index.js +1 -1
  200. package/dist/features/transcript/feature.js +1 -1
  201. package/dist/features/transcript/index.js +2 -2
  202. package/dist/i18n/de/ai.js +2 -0
  203. package/dist/i18n/de/ai.js.map +1 -1
  204. package/dist/i18n/de/composites.js +8 -1
  205. package/dist/i18n/de/composites.js.map +1 -1
  206. package/dist/i18n/de/tree.js +14 -1
  207. package/dist/i18n/de/tree.js.map +1 -1
  208. package/dist/i18n/de.d.ts +22 -0
  209. package/dist/i18n/en/ai.js +2 -0
  210. package/dist/i18n/en/ai.js.map +1 -1
  211. package/dist/i18n/en/composites.js +8 -1
  212. package/dist/i18n/en/composites.js.map +1 -1
  213. package/dist/i18n/en/tree.js +14 -1
  214. package/dist/i18n/en/tree.js.map +1 -1
  215. package/dist/integrations/composite-ai/composite-fill.js +2 -2
  216. package/dist/integrations/composite-ai/composite-summary.js +2 -2
  217. package/dist/integrations/composite-ai/index.js +2 -2
  218. package/dist/integrations/transcript-ai/transcript-cleanup.js +1 -1
  219. package/dist/markdown/MarkdownSurface.d.ts +1 -1
  220. package/dist/markdown/MarkdownSurface.js +2 -2
  221. package/dist/markdown/MarkdownSurface.vue.d.ts +7 -7
  222. package/dist/markdown/index.d.ts +2 -2
  223. package/dist/nuxt/runtime/server/routes/collab/[id].js +1 -1
  224. package/dist/react/EditorSurface.d.ts +1 -1
  225. package/dist/react/EditorSurface.js +43 -149
  226. package/dist/react/EditorSurface.js.map +1 -1
  227. package/dist/react/EditorToolbar.js +3 -40
  228. package/dist/react/EditorToolbar.js.map +1 -1
  229. package/dist/schema/advanced.js +2 -2
  230. package/dist/server/adapter.d.ts +2 -2
  231. package/dist/server/collab.js +1 -1
  232. package/dist/server/handlers/ai.d.ts +4 -2
  233. package/dist/server/handlers/ai.js +11 -9
  234. package/dist/server/handlers/ai.js.map +1 -1
  235. package/dist/server/handlers/context.d.ts +1 -1
  236. package/dist/server/handlers/editor.d.ts +1 -1
  237. package/dist/server/handlers/editor.js +2 -2
  238. package/dist/server/host.d.ts +1 -1
  239. package/dist/server/index.d.ts +2 -2
  240. package/dist/settings/index.d.ts +1 -1
  241. package/dist/styles/tokens.css +1 -1
  242. package/dist/tree/DocumentTree.js +159 -387
  243. package/dist/tree/DocumentTree.js.map +1 -1
  244. package/dist/tree/drop-rules.js +37 -0
  245. package/dist/tree/drop-rules.js.map +1 -0
  246. package/dist/tree/react/DocumentTree.d.ts +2 -2
  247. package/dist/tree/react/DocumentTree.js +135 -397
  248. package/dist/tree/react/DocumentTree.js.map +1 -1
  249. package/dist/tree/react/use-tree-model.js +129 -0
  250. package/dist/tree/react/use-tree-model.js.map +1 -0
  251. package/dist/tree/react.js +1 -1
  252. package/dist/tree/tree-host.js +100 -0
  253. package/dist/tree/tree-host.js.map +1 -0
  254. package/dist/tree/tree-items.js +22 -43
  255. package/dist/tree/tree-items.js.map +1 -1
  256. package/dist/tree/tree-model.js +580 -0
  257. package/dist/tree/tree-model.js.map +1 -0
  258. package/dist/tree/tree-rows.js +77 -0
  259. package/dist/tree/tree-rows.js.map +1 -0
  260. package/dist/tree/use-tree-model.js +94 -0
  261. package/dist/tree/use-tree-model.js.map +1 -0
  262. package/dist/tree/vue.js +1 -1
  263. package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
  264. package/dist/vue/SimpleEditor.vue.d.ts +2 -2
  265. package/dist/vue/components/EditorSurface.js +27 -87
  266. package/dist/vue/components/EditorSurface.js.map +1 -1
  267. package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
  268. package/dist/vue/components/EditorToolbar.js +7 -44
  269. package/dist/vue/components/EditorToolbar.js.map +1 -1
  270. package/dist/vue/context.d.ts +1 -1
  271. package/dist/vue/context.js +1 -1
  272. package/dist/vue/context.js.map +1 -1
  273. package/dist/vue/use-editor-ui.d.ts +1 -1
  274. package/dist/vue/use-notifier.d.ts +1 -1
  275. package/docs/integration.md +97 -4
  276. package/docs/llms.md +9 -1
  277. package/docs/usage.md +10 -5
  278. package/package.json +67 -5
@@ -0,0 +1,139 @@
1
+ import { presetExtension } from "./pipeline.js";
2
+ import { createMetadataRegistry } from "./metadata.js";
3
+ import { SuggestionPopup } from "./suggestion-popup.js";
4
+ import { contributions } from "./define-editor.js";
5
+ import { registerEditor } from "./editor-ui.js";
6
+ import { blockActionsOf, createFeatureContext } from "./feature-context.js";
7
+ import { EMPTY_EDITOR_CONTENT } from "./schema/index.js";
8
+ import { getSchema } from "@tiptap/core";
9
+ //#region src/core/surface-session.ts
10
+ /**
11
+ * Builds the session of one mounted surface: metadata registry, feature context, the extension list, the block actions
12
+ * and the initial document. Throws when `collaboration` is given and the preset has no collaboration feature. Extension
13
+ * order is fixed: the preset extension, the suggestion popup, the features' extensions in feature order, the host's.
14
+ * The document is checked as `preset.pipeline.migrateContent` followed by the schema's `check()`; a failure is an error
15
+ * value, never a throw.
16
+ */
17
+ function createSurfaceSession(options) {
18
+ const { app, preset, ui, collaboration, getEditor } = options;
19
+ if (collaboration && !preset.features.some((feature) => feature.id === "collab")) throw new Error("@ecsplico/editor: `collaboration` needs a preset with the collaboration feature");
20
+ const registry = createMetadataRegistry({ onCreate: (list, item) => options.onMetadataCreate?.(list, item) });
21
+ registry.replace({ ...options.metadata });
22
+ const ctx = createFeatureContext({
23
+ app,
24
+ preset,
25
+ ui,
26
+ metadata: registry,
27
+ notify: options.notify,
28
+ getEditor,
29
+ ...options.documentId !== void 0 && { documentId: options.documentId },
30
+ ...options.upload && { upload: options.upload },
31
+ ...collaboration && { collaboration },
32
+ ...options.abilities && { abilities: options.abilities },
33
+ ...options.slashCommands && { slashCommands: options.slashCommands }
34
+ });
35
+ const extensions = [
36
+ presetExtension(preset),
37
+ SuggestionPopup.configure({ renderer: options.suggestionRenderer }),
38
+ ...contributions(preset, "extensions").flatMap((factory) => factory(ctx)),
39
+ ...options.extensions ?? []
40
+ ];
41
+ const schema = getSchema(extensions);
42
+ const overlays = preset.features.flatMap((feature) => [...feature.overlays ?? []]);
43
+ const blockActions = blockActionsOf(preset, ctx, options.blockActions);
44
+ const validated = (json) => {
45
+ try {
46
+ const migrated = preset.pipeline.migrateContent(json);
47
+ schema.nodeFromJSON(migrated).check();
48
+ return { json: migrated };
49
+ } catch (error) {
50
+ return { error: error instanceof Error ? error : new Error(String(error)) };
51
+ }
52
+ };
53
+ let lastEmitted;
54
+ let pendingContentError;
55
+ const editorOptions = {
56
+ extensions,
57
+ editable: options.editable ?? true,
58
+ editorProps: { attributes: { class: "tiptap" } }
59
+ };
60
+ if (!collaboration) {
61
+ const initial = options.value ? validated(options.value) : { json: EMPTY_EDITOR_CONTENT };
62
+ if ("error" in initial) pendingContentError = initial.error;
63
+ editorOptions.content = "json" in initial ? initial.json : EMPTY_EDITOR_CONTENT;
64
+ editorOptions.onUpdate = ({ editor }) => {
65
+ lastEmitted = editor.getJSON();
66
+ options.onChange?.(lastEmitted);
67
+ };
68
+ }
69
+ let appliedValue = options.value;
70
+ let appliedEditable = options.editable ?? true;
71
+ let appliedMetadata = options.metadata;
72
+ let teardowns;
73
+ let announced;
74
+ return {
75
+ editorOptions,
76
+ overlays,
77
+ activate() {
78
+ if (teardowns) return;
79
+ const active = [];
80
+ teardowns = active;
81
+ for (const run of contributions(preset, "setup")) {
82
+ const teardown = run(ctx);
83
+ if (teardown) active.push(teardown);
84
+ }
85
+ if (pendingContentError) {
86
+ const error = pendingContentError;
87
+ pendingContentError = void 0;
88
+ options.onContentError?.(error);
89
+ }
90
+ },
91
+ deactivate() {
92
+ const active = teardowns;
93
+ teardowns = void 0;
94
+ for (const teardown of active ?? []) teardown();
95
+ },
96
+ register(editor) {
97
+ const unregister = registerEditor(editor, {
98
+ ui,
99
+ preset,
100
+ ctx,
101
+ blockActions
102
+ });
103
+ if (announced !== editor) {
104
+ announced = editor;
105
+ options.onReady?.(editor);
106
+ }
107
+ return unregister;
108
+ },
109
+ setValue(value) {
110
+ if (value === appliedValue) return;
111
+ appliedValue = value;
112
+ const editor = getEditor();
113
+ if (collaboration || !value || !editor || editor.isDestroyed || value === lastEmitted) return;
114
+ const checked = validated(value);
115
+ if ("error" in checked) {
116
+ options.onContentError?.(checked.error);
117
+ return;
118
+ }
119
+ editor.commands.setContent(checked.json, { emitUpdate: false });
120
+ },
121
+ setEditable(editable) {
122
+ if (editable === appliedEditable) return;
123
+ appliedEditable = editable;
124
+ const editor = getEditor();
125
+ if (!editor || editor.isDestroyed) return;
126
+ editor.setEditable(editable, false);
127
+ editor.view.dispatch(editor.state.tr.setMeta("addToHistory", false));
128
+ },
129
+ setMetadata(metadata) {
130
+ if (metadata === appliedMetadata) return;
131
+ appliedMetadata = metadata;
132
+ registry.replace({ ...metadata });
133
+ }
134
+ };
135
+ }
136
+ //#endregion
137
+ export { createSurfaceSession };
138
+
139
+ //# sourceMappingURL=surface-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"surface-session.js","names":[],"sources":["../../src/core/surface-session.ts"],"sourcesContent":["import { type AnyExtension, type Editor, type EditorOptions, getSchema, type JSONContent } from '@tiptap/core'\nimport type { EditorContext } from './context'\nimport { contributions } from './define-editor'\nimport { type EditorUi, registerEditor } from './editor-ui'\nimport type { AbilitiesOptions, BlockAction, SlashCommandItem } from './extension-points'\nimport type { CollaborationSession, EditorPreset, UploadFn } from './feature'\nimport { blockActionsOf, createFeatureContext } from './feature-context'\nimport { createMetadataRegistry } from './metadata'\nimport type { EditorMetadata, MetadataItem } from './metadata-types'\nimport type { Notifier } from './notify'\nimport { presetExtension } from './pipeline'\nimport { EMPTY_EDITOR_CONTENT } from './schema'\nimport { SuggestionPopup, type SuggestionPopupRenderer } from './suggestion-popup'\n\n/**\n * What a surface hands its session. The framework layer owns everything that lives in its reactivity: the chrome\n * (`ui`), the notice renderer, the suggestion popup renderer and the editor instance (`getEditor`).\n * `preset`, `collaboration`, `documentId`, `upload`, `abilities`, `extensions`, `blockActions`, `slashCommands`, `value` and\n * `editable` are read once, when the session is created; later changes reach it through `setValue`, `setEditable` and\n * `setMetadata`.\n */\nexport interface SurfaceSessionOptions<C = unknown> {\n app: EditorContext\n /** The features the editor is made of. With `collaboration` it must contain the collaboration feature (else creating the session throws). */\n preset: EditorPreset<C>\n /** The chrome the features reach through `FeatureContext.ui`; registered for the editor by `register`. */\n ui: EditorUi\n /** The surface's notice renderer (`FeatureContext.notify`). */\n notify: Notifier\n /** The framework's popup for `@` / `#` / `/` suggestions. */\n suggestionRenderer: SuggestionPopupRenderer\n /** The mounted editor, `undefined` until the framework layer created it; used by `FeatureContext.getEditor`, `setValue` and `setEditable`. */\n getEditor: () => Editor | undefined\n /** Collaborative mode: the Y.Doc is the document, so there is no `value` and no `onChange`. */\n collaboration?: CollaborationSession | undefined\n /** Standalone initial document (TipTap JSON, unproxied); absent = an empty document. */\n value?: JSONContent | undefined\n /** Default `true`. */\n editable?: boolean | undefined\n /** The host's lists; later ones go through `setMetadata`. */\n metadata?: EditorMetadata | undefined\n documentId?: string | undefined\n upload?: UploadFn | undefined\n abilities?: Partial<AbilitiesOptions> | undefined\n slashCommands?: ((defaults: SlashCommandItem[]) => SlashCommandItem[]) | undefined\n /** Added after the features' extensions. */\n extensions?: readonly AnyExtension[] | undefined\n /** Added after the features' block actions. */\n blockActions?: readonly BlockAction[] | undefined\n /** Standalone: the document after every edit (the JSON the session remembers as its own, see `setValue`). */\n onChange?: ((content: JSONContent) => void) | undefined\n /** A new item was created in a metadata list. */\n onMetadataCreate?: ((list: string, item: MetadataItem) => void) | undefined\n /** A document handed in (initially or through `setValue`) does not fit the schema. */\n onContentError?: ((error: Error) => void) | undefined\n /** A mounted editor was registered for the first time (see `register`). */\n onReady?: ((editor: Editor) => void) | undefined\n}\n\n/**\n * Everything of an `EditorSurface` that does not depend on the UI framework: the preset's extensions and context,\n * the document's validation and echo handling, the features' `setup` lifecycle and the registration of the editor's\n * chrome. The framework layer creates the session once (state of the component instance), mounts a TipTap editor from\n * `editorOptions`, and forwards prop changes and lifecycle moments to it.\n */\nexport interface SurfaceSession<C = unknown> {\n /**\n * Options for the TipTap editor. Standalone mode (no `collaboration`) carries the initial document (`content`, already\n * migrated; an empty document when the given one was rejected) and the `onUpdate` that feeds `onChange`. The framework\n * layer may add its own options (rendering flags) but must not replace these.\n */\n readonly editorOptions: Partial<EditorOptions> & { extensions: AnyExtension[] }\n /** The features' overlay components, in feature order; the framework layer renders them beside the editor with `{ editor }`. */\n readonly overlays: readonly C[]\n /**\n * Starts the features' `setup` contributions, then reports a rejected initial document through `onContentError` (once\n * per session, not per activation). A no-op while active. Pair every activation with `deactivate`: React StrictMode\n * mounts, cleans up and mounts again, which must leave exactly one active setup.\n */\n activate: () => void\n /** Runs the teardowns of the active `setup` contributions, in the order they were started; a no-op while inactive. */\n deactivate: () => void\n /**\n * Registers the chrome of `editor` (the raw instance, see `registerEditor`) and announces it through `onReady` the\n * first time this editor is registered, so a host toolbar that receives it finds the chrome. Returns the unregister\n * function (idempotent). Call it again after the framework layer replaced the editor instance.\n */\n register: (editor: Editor) => () => void\n /**\n * The host's document changed (standalone only; ignored with `collaboration`). Not an edit: no `onChange`, no update\n * event. Ignored when `value` is the very object `onChange` last received (the host handing the surface's own JSON\n * back), is absent, or was already given; otherwise it is migrated and checked against the schema and applied, or\n * reported through `onContentError` with the current document kept. A missing or destroyed editor ignores it.\n */\n setValue: (value: JSONContent | undefined) => void\n /**\n * Switches the editor's editable state; ignored when it equals the last given one, and while no (or a destroyed)\n * editor exists. TipTap's `setEditable(…, false)` changes the flag without any event, so it is followed by an empty\n * `addToHistory: false` transaction: components that follow `isEditable` on `transaction` (block UI, the composite\n * inspector) notice. It has no steps, so nothing syncs and the history is untouched.\n */\n setEditable: (editable: boolean) => void\n /** Replaces the host's metadata lists; ignored when `metadata` is the object that was last given. */\n setMetadata: (metadata: EditorMetadata | undefined) => void\n}\n\n/**\n * Builds the session of one mounted surface: metadata registry, feature context, the extension list, the block actions\n * and the initial document. Throws when `collaboration` is given and the preset has no collaboration feature. Extension\n * order is fixed: the preset extension, the suggestion popup, the features' extensions in feature order, the host's.\n * The document is checked as `preset.pipeline.migrateContent` followed by the schema's `check()`; a failure is an error\n * value, never a throw.\n */\nexport function createSurfaceSession<C = unknown>(options: SurfaceSessionOptions<C>): SurfaceSession<C> {\n const { app, preset, ui, collaboration, getEditor } = options\n if (collaboration && !preset.features.some(feature => feature.id === 'collab')) {\n throw new Error('@ecsplico/editor: `collaboration` needs a preset with the collaboration feature')\n }\n\n const registry = createMetadataRegistry({ onCreate: (list, item) => options.onMetadataCreate?.(list, item) })\n registry.replace({ ...options.metadata })\n\n const ctx = createFeatureContext({\n app,\n preset,\n ui,\n metadata: registry,\n notify: options.notify,\n getEditor,\n ...(options.documentId !== undefined && { documentId: options.documentId }),\n ...(options.upload && { upload: options.upload }),\n ...(collaboration && { collaboration }),\n ...(options.abilities && { abilities: options.abilities }),\n ...(options.slashCommands && { slashCommands: options.slashCommands }),\n })\n\n const extensions: AnyExtension[] = [\n presetExtension(preset),\n SuggestionPopup.configure({ renderer: options.suggestionRenderer }),\n ...contributions(preset, 'extensions').flatMap(factory => factory(ctx)),\n ...(options.extensions ?? []),\n ]\n const schema = getSchema(extensions)\n const overlays = preset.features.flatMap(feature => [...(feature.overlays ?? [])])\n const blockActions = blockActionsOf(preset, ctx, options.blockActions)\n\n const validated = (json: JSONContent): { json: JSONContent } | { error: Error } => {\n try {\n const migrated = preset.pipeline.migrateContent(json)\n schema.nodeFromJSON(migrated).check()\n return { json: migrated }\n }\n catch (error) {\n return { error: error instanceof Error ? error : new Error(String(error)) }\n }\n }\n\n // Standalone: the JSON last sent through `onChange`; the host handing it back is not an external change.\n let lastEmitted: JSONContent | undefined\n let pendingContentError: Error | undefined\n const editorOptions: SurfaceSession<C>['editorOptions'] = {\n extensions,\n editable: options.editable ?? true,\n editorProps: { attributes: { class: 'tiptap' } },\n }\n if (!collaboration) {\n const initial = options.value ? validated(options.value) : { json: EMPTY_EDITOR_CONTENT }\n if ('error' in initial) pendingContentError = initial.error\n editorOptions.content = 'json' in initial ? initial.json : EMPTY_EDITOR_CONTENT\n editorOptions.onUpdate = ({ editor }) => {\n lastEmitted = editor.getJSON()\n options.onChange?.(lastEmitted)\n }\n }\n\n // What the setters below already applied; they only react to a change.\n let appliedValue = options.value\n let appliedEditable = options.editable ?? true\n let appliedMetadata = options.metadata\n // Teardowns of the `setup` contributions that are currently active; undefined while inactive.\n let teardowns: (() => void)[] | undefined\n let announced: Editor | undefined\n\n return {\n editorOptions,\n overlays,\n activate() {\n if (teardowns) return\n const active: (() => void)[] = []\n teardowns = active\n for (const run of contributions(preset, 'setup')) {\n const teardown = run(ctx)\n if (teardown) active.push(teardown)\n }\n if (pendingContentError) {\n const error = pendingContentError\n pendingContentError = undefined\n options.onContentError?.(error)\n }\n },\n deactivate() {\n const active = teardowns\n teardowns = undefined\n for (const teardown of active ?? []) teardown()\n },\n register(editor) {\n const unregister = registerEditor(editor, { ui, preset, ctx, blockActions })\n if (announced !== editor) {\n announced = editor\n options.onReady?.(editor)\n }\n return unregister\n },\n setValue(value) {\n if (value === appliedValue) return\n appliedValue = value\n const editor = getEditor()\n if (collaboration || !value || !editor || editor.isDestroyed || value === lastEmitted) return\n const checked = validated(value)\n if ('error' in checked) {\n options.onContentError?.(checked.error)\n return\n }\n editor.commands.setContent(checked.json, { emitUpdate: false })\n },\n setEditable(editable) {\n if (editable === appliedEditable) return\n appliedEditable = editable\n const editor = getEditor()\n if (!editor || editor.isDestroyed) return\n editor.setEditable(editable, false)\n editor.view.dispatch(editor.state.tr.setMeta('addToHistory', false))\n },\n setMetadata(metadata) {\n if (metadata === appliedMetadata) return\n appliedMetadata = metadata\n registry.replace({ ...metadata })\n },\n }\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { MarkdownSurfaceProps } from "../markdown/MarkdownSurface.js";
2
+ import { Editor } from "@tiptap/core";
2
3
  import { EditorView } from "@codemirror/view";
3
4
  import { ReactNode } from "react";
4
- import { Editor } from "@tiptap/core";
5
5
  //#region src/document/DocumentSurface.d.ts
6
6
  /** Which surface was mounted and its editor, for `onReady`. */
7
7
  export type DocumentSurfaceTarget = {
@@ -32,16 +32,16 @@ type __VLS_Slots = {} & { [K in NonNullable<typeof __VLS_13>]?: (props: typeof _
32
32
  binary?: (props: typeof __VLS_26) => any;
33
33
  };
34
34
  declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
35
- ready: (target: DocumentSurfaceTarget) => any;
36
35
  "update:modelValue": (text: string) => any;
36
+ ready: (target: DocumentSurfaceTarget) => any;
37
37
  save: (text: string) => any;
38
38
  }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
39
- onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
40
39
  "onUpdate:modelValue"?: ((text: string) => any) | undefined;
40
+ onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
41
41
  onSave?: ((text: string) => any) | undefined;
42
42
  }>, {
43
- modelValue: string;
44
43
  editable: boolean;
44
+ modelValue: string;
45
45
  wordWrap: boolean;
46
46
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
47
47
  declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
@@ -1,9 +1,9 @@
1
1
  import { Translate } from "../../i18n/index.js";
2
2
  import { Notifier } from "../../core/notify.js";
3
3
  import { ReadonlyStore } from "../../core/store.js";
4
- import { AiReviewOutcome } from "./ai-review.js";
5
4
  import { AgentOp, AgentOpFeedback, AgentOpOutcome } from "./protocol/agent-ops.js";
6
5
  import "./protocol/index.js";
6
+ import { AiReviewOutcome } from "./ai-review.js";
7
7
  import { AgentScope } from "./agent-ops.js";
8
8
  import { Editor } from "@tiptap/core";
9
9
  //#region src/features/ai/agent-op-review.d.ts
@@ -32,7 +32,7 @@ export interface AgentOpReview {
32
32
  /**
33
33
  * Opens the in-document review of `ops`: every operation is checked against `scope` (the selected blocks; null:
34
34
  * the whole document) and the document as it is now. Returns the outcome of each operation, in order: `pending`
35
- * while it waits for the user's decision, otherwise why nothing is shown (`blocked`, `missing`, `unchanged`,
35
+ * while it waits for the user's decision, otherwise why nothing is shown (`blocked`, `missing`, `stale`, `unchanged`,
36
36
  * `overlap`, `empty`). Operation IDs (`op`) must be unique within this review.
37
37
  */
38
38
  propose: (editor: Editor, ops: AgentOp[], scope: AgentScope | null) => AgentOpOutcome[];
@@ -1 +1 @@
1
- {"version":3,"file":"agent-op-review.js","names":[],"sources":["../../../src/features/ai/agent-op-review.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { releaseRanges } from '../../core'\nimport type { Notifier } from '../../core/notify'\nimport { createStore, type ReadonlyStore } from '../../core/store'\nimport type { Translate } from '../../i18n'\nimport { type AgentScope, agentOpChange, agentOpSnippet } from './agent-ops'\nimport { type AiReviewChange, type AiReviewDecision, type AiReviewHandle, type AiReviewOutcome, acceptAiReview, aiReviewKey, rejectAiReview, setActiveAiReviewHunk, startAiReview } from './ai-review'\nimport { type AgentOp, type AgentOpFeedback, type AgentOpOutcome, agentOpTarget } from './protocol'\n\n/** Client state of a proposed operation: its outcome so far, the target's text, its review hunks. */\nexport interface AgentOpState {\n outcome: AgentOpOutcome\n snippet: string\n hunks: string[]\n decided: Record<AiReviewOutcome, number>\n /** Its final outcome went to the model with a user message. */\n reported: boolean\n}\n\n/** Snapshot of the proposed operations; every change replaces `ops` (never mutated in place). */\nexport interface AgentOpReviewState {\n /** Operation states by operation id. */\n ops: Readonly<Record<string, AgentOpState>>\n}\n\nexport interface AgentOpReviewOptions {\n /** Receives the notice of a decision that could not be applied because the text changed underneath. */\n notify: Notifier\n /** The app's translator (`EditorContext.t`): that notice and the snippets of the operations' target blocks. */\n t: Translate\n}\n\nexport interface AgentOpReview {\n state: ReadonlyStore<AgentOpReviewState>\n /**\n * Opens the in-document review of `ops`: every operation is checked against `scope` (the selected blocks; null:\n * the whole document) and the document as it is now. Returns the outcome of each operation, in order: `pending`\n * while it waits for the user's decision, otherwise why nothing is shown (`blocked`, `missing`, `unchanged`,\n * `overlap`, `empty`). Operation IDs (`op`) must be unique within this review.\n */\n propose: (editor: Editor, ops: AgentOp[], scope: AgentScope | null) => AgentOpOutcome[]\n /** Accepts or rejects the open hunks of operation `id`. */\n decide: (editor: Editor, id: string, accept: boolean) => void\n /** Scrolls to operation `op`: its first open hunk (selected for accept/reject), else its target block. */\n reveal: (editor: Editor, op: AgentOp) => boolean\n /**\n * What the user decided since the last call: every operation whose outcome the model has not heard yet (open ones\n * too, as `pending`). Operations with a final outcome count as reported afterwards, so the next call omits them.\n */\n feedback: () => AgentOpFeedback[]\n /** Like `feedback`, but reports nothing as heard: for a transport that marks the operations only once its message went out (`markReported`). */\n unreported: () => AgentOpFeedback[]\n /** Marks the operations `ids` as heard by the model; unknown IDs and operations that are still pending are left alone. */\n markReported: (ids: readonly string[]) => void\n /** The number the next operation ID of a conversation-wide sequence (`op1`, `op2`, …) starts at; never reset. */\n nextOp: () => number\n /** Advances `nextOp` past `count` operations that were given IDs. */\n advanceOp: (count: number) => void\n /** Forgets every operation state and feedback; reviews still open in an editor stay open and their decisions are ignored from now on. */\n reset: () => void\n /** Rejects the reviews still open in `editor` that this one started, then forgets everything (`reset`). */\n end: (editor: Editor) => void\n}\n\n/** The state without operations; shared so an emptied review and an emptied chat hold the identical object. */\nexport const NO_AGENT_OPS: AgentOpReviewState['ops'] = Object.freeze({})\n\n/**\n * The review of agent edit operations, framework free: the chat's own transport and a host's (an agent working\n * elsewhere that proposes edits through an action) drive it the same way. `propose` shows the operations as in-document\n * hunks the user accepts or rejects (AiReviewBar); `state` follows the decisions; `feedback` reads them back.\n * Client only: needs the `AiReview` extension in the editor.\n */\nexport function createAgentOpReview({ notify, t }: AgentOpReviewOptions): AgentOpReview {\n const store = createStore<AgentOpReviewState>({ ops: NO_AGENT_OPS })\n // What `AgentOpFeedback` names beyond the state: the operation's type and target block.\n const targets = new Map<string, Pick<AgentOpFeedback, 'type' | 'target'>>()\n const open = new Set<AiReviewHandle>()\n // Never reset, so operation IDs stay unique even for reviews still open from an earlier conversation.\n let nextOp = 1\n\n const patch = (ops: Readonly<Record<string, AgentOpState>>) => store.set({ ops })\n\n function onDecision(decisions: AiReviewDecision[]) {\n const ops = { ...store.get().ops }\n for (const { changeId, outcome } of decisions) {\n const current = ops[changeId]\n if (!current) continue // from a conversation that was reset\n const decided = { ...current.decided, [outcome]: current.decided[outcome] + 1 }\n const { accepted, rejected, invalid } = decided\n const state: AgentOpState = { ...current, decided }\n if (accepted + rejected + invalid >= state.hunks.length) {\n const kinds = (['accepted', 'rejected', 'invalid'] as const).filter(kind => decided[kind] > 0)\n state.outcome = kinds.length === 1 ? kinds[0]! : 'partial'\n }\n ops[changeId] = state\n if (outcome === 'invalid') notify({ id: 'ai-review-invalid', title: t('ai.op.invalid.title'), description: t('ai.op.invalid.description'), kind: 'info' })\n }\n patch(ops)\n }\n\n function propose(editor: Editor, proposed: AgentOp[], scope: AgentScope | null): AgentOpOutcome[] {\n const allowed = scope ? new Set(scope.ids) : null\n const changes: AiReviewChange[] = []\n const states: Record<string, AgentOpState> = {}\n for (const op of proposed) {\n const change = agentOpChange(editor, op, allowed)\n states[op.op] = {\n outcome: typeof change === 'string' ? change : 'pending',\n snippet: agentOpSnippet(editor.state.doc, op, t),\n hunks: [],\n decided: { accepted: 0, rejected: 0, invalid: 0 },\n reported: false,\n }\n targets.set(op.op, { type: op.type, target: agentOpTarget(op) })\n if (typeof change !== 'string') changes.push(change)\n }\n // The states are published before the review starts: it may report decisions synchronously.\n patch({ ...store.get().ops, ...states })\n if (changes.length) {\n let started: AiReviewHandle | undefined\n const review = startAiReview(editor, changes, {\n onDecision,\n onDone: () => { if (started) open.delete(started) },\n })\n started = review\n if (review.hunks) open.add(review)\n // The review keeps anchors of its own.\n releaseRanges(editor, changes.map(c => c.kind === 'insert' ? c.at : c.range))\n const ops = { ...store.get().ops }\n for (const { id, reason } of review.skipped) {\n if (ops[id]) ops[id] = { ...ops[id], outcome: reason }\n }\n for (const hunk of aiReviewKey.getState(editor.state)?.hunks ?? []) {\n const state = hunk.reviewId === review.id ? ops[hunk.changeId] : undefined\n if (state) ops[hunk.changeId] = { ...state, hunks: [...state.hunks, hunk.id] }\n }\n patch(ops)\n }\n const { ops } = store.get()\n return proposed.map(op => ops[op.op]!.outcome)\n }\n\n function decide(editor: Editor, id: string, accept: boolean) {\n const hunks = store.get().ops[id]?.hunks\n if (!hunks?.length) return\n if (accept) acceptAiReview(editor, hunks)\n else rejectAiReview(editor, hunks)\n }\n\n function reveal(editor: Editor, op: AgentOp): boolean {\n const hunks = store.get().ops[op.op]?.hunks\n const pending = aiReviewKey.getState(editor.state)?.hunks.find(hunk => hunks?.includes(hunk.id))\n if (pending) {\n setActiveAiReviewHunk(editor.view, pending.id)\n editor.view.dom.querySelector(`[data-ai-hunk=\"${pending.id}\"]`)?.scrollIntoView({ block: 'center', behavior: 'smooth' })\n return true\n }\n const target = agentOpTarget(op)\n return target !== null && editor.commands.revealBlock(target)\n }\n\n /** Operations whose final outcome the model has not heard yet (open ones too, as „noch nicht entschieden“), in proposal order. */\n function unreported(): AgentOpFeedback[] {\n return Object.entries(store.get().ops).flatMap(([op, state]) => {\n const known = targets.get(op)\n return known && !state.reported ? [{ op, ...known, outcome: state.outcome }] : []\n })\n }\n\n function markReported(ids: readonly string[]) {\n const { ops } = store.get()\n if (!ids.some(id => ops[id] && ops[id].outcome !== 'pending')) return\n const next = { ...ops }\n for (const id of ids) {\n if (next[id] && next[id].outcome !== 'pending') next[id] = { ...next[id], reported: true }\n }\n patch(next)\n }\n\n function feedback(): AgentOpFeedback[] {\n const heard = unreported()\n markReported(heard.map(({ op }) => op))\n return heard\n }\n\n function reset() {\n targets.clear()\n open.clear()\n patch(NO_AGENT_OPS)\n }\n\n function end(editor: Editor) {\n if (!editor.isDestroyed) for (const review of [...open]) review.cancel()\n reset()\n }\n\n return { state: store, propose, decide, reveal, feedback, unreported, markReported, nextOp: () => nextOp, advanceOp: (count) => { nextOp += count }, reset, end }\n}\n"],"mappings":";;;;;;;AAiEA,MAAa,eAA0C,OAAO,OAAO,CAAC,CAAC;;;;;;;AAQvE,SAAgB,oBAAoB,EAAE,QAAQ,KAA0C;CACtF,MAAM,QAAQ,YAAgC,EAAE,KAAK,aAAa,CAAC;CAEnE,MAAM,0BAAU,IAAI,IAAsD;CAC1E,MAAM,uBAAO,IAAI,IAAoB;CAErC,IAAI,SAAS;CAEb,MAAM,SAAS,QAAgD,MAAM,IAAI,EAAE,IAAI,CAAC;CAEhF,SAAS,WAAW,WAA+B;EACjD,MAAM,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,CAAC,IAAI;EACjC,KAAK,MAAM,EAAE,UAAU,aAAa,WAAW;GAC7C,MAAM,UAAU,IAAI;GACpB,IAAI,CAAC,SAAS;GACd,MAAM,UAAU;IAAE,GAAG,QAAQ;KAAU,UAAU,QAAQ,QAAQ,WAAW;GAAE;GAC9E,MAAM,EAAE,UAAU,UAAU,YAAY;GACxC,MAAM,QAAsB;IAAE,GAAG;IAAS;GAAQ;GAClD,IAAI,WAAW,WAAW,WAAW,MAAM,MAAM,QAAQ;IACvD,MAAM,QAAS;KAAC;KAAY;KAAY;IAAS,CAAC,CAAW,QAAO,SAAQ,QAAQ,QAAQ,CAAC;IAC7F,MAAM,UAAU,MAAM,WAAW,IAAI,MAAM,KAAM;GACnD;GACA,IAAI,YAAY;GAChB,IAAI,YAAY,WAAW,OAAO;IAAE,IAAI;IAAqB,OAAO,EAAE,qBAAqB;IAAG,aAAa,EAAE,2BAA2B;IAAG,MAAM;GAAO,CAAC;EAC3J;EACA,MAAM,GAAG;CACX;CAEA,SAAS,QAAQ,QAAgB,UAAqB,OAA4C;EAChG,MAAM,UAAU,QAAQ,IAAI,IAAI,MAAM,GAAG,IAAI;EAC7C,MAAM,UAA4B,CAAC;EACnC,MAAM,SAAuC,CAAC;EAC9C,KAAK,MAAM,MAAM,UAAU;GACzB,MAAM,SAAS,cAAc,QAAQ,IAAI,OAAO;GAChD,OAAO,GAAG,MAAM;IACd,SAAS,OAAO,WAAW,WAAW,SAAS;IAC/C,SAAS,eAAe,OAAO,MAAM,KAAK,IAAI,CAAC;IAC/C,OAAO,CAAC;IACR,SAAS;KAAE,UAAU;KAAG,UAAU;KAAG,SAAS;IAAE;IAChD,UAAU;GACZ;GACA,QAAQ,IAAI,GAAG,IAAI;IAAE,MAAM,GAAG;IAAM,QAAQ,cAAc,EAAE;GAAE,CAAC;GAC/D,IAAI,OAAO,WAAW,UAAU,QAAQ,KAAK,MAAM;EACrD;EAEA,MAAM;GAAE,GAAG,MAAM,IAAI,CAAC,CAAC;GAAK,GAAG;EAAO,CAAC;EACvC,IAAI,QAAQ,QAAQ;GAClB,IAAI;GACJ,MAAM,SAAS,cAAc,QAAQ,SAAS;IAC5C;IACA,cAAc;KAAE,IAAI,SAAS,KAAK,OAAO,OAAO;IAAE;GACpD,CAAC;GACD,UAAU;GACV,IAAI,OAAO,OAAO,KAAK,IAAI,MAAM;GAEjC,cAAc,QAAQ,QAAQ,KAAI,MAAK,EAAE,SAAS,WAAW,EAAE,KAAK,EAAE,KAAK,CAAC;GAC5E,MAAM,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,CAAC,IAAI;GACjC,KAAK,MAAM,EAAE,IAAI,YAAY,OAAO,SAClC,IAAI,IAAI,KAAK,IAAI,MAAM;IAAE,GAAG,IAAI;IAAK,SAAS;GAAO;GAEvD,KAAK,MAAM,QAAQ,YAAY,SAAS,OAAO,KAAK,CAAC,EAAE,SAAS,CAAC,GAAG;IAClE,MAAM,QAAQ,KAAK,aAAa,OAAO,KAAK,IAAI,KAAK,YAAY,KAAA;IACjE,IAAI,OAAO,IAAI,KAAK,YAAY;KAAE,GAAG;KAAO,OAAO,CAAC,GAAG,MAAM,OAAO,KAAK,EAAE;IAAE;GAC/E;GACA,MAAM,GAAG;EACX;EACA,MAAM,EAAE,QAAQ,MAAM,IAAI;EAC1B,OAAO,SAAS,KAAI,OAAM,IAAI,GAAG,GAAG,CAAE,OAAO;CAC/C;CAEA,SAAS,OAAO,QAAgB,IAAY,QAAiB;EAC3D,MAAM,QAAQ,MAAM,IAAI,CAAC,CAAC,IAAI,GAAG,EAAE;EACnC,IAAI,CAAC,OAAO,QAAQ;EACpB,IAAI,QAAQ,eAAe,QAAQ,KAAK;OACnC,eAAe,QAAQ,KAAK;CACnC;CAEA,SAAS,OAAO,QAAgB,IAAsB;EACpD,MAAM,QAAQ,MAAM,IAAI,CAAC,CAAC,IAAI,GAAG,GAAG,EAAE;EACtC,MAAM,UAAU,YAAY,SAAS,OAAO,KAAK,CAAC,EAAE,MAAM,MAAK,SAAQ,OAAO,SAAS,KAAK,EAAE,CAAC;EAC/F,IAAI,SAAS;GACX,sBAAsB,OAAO,MAAM,QAAQ,EAAE;GAC7C,OAAO,KAAK,IAAI,cAAc,kBAAkB,QAAQ,GAAG,GAAG,CAAC,EAAE,eAAe;IAAE,OAAO;IAAU,UAAU;GAAS,CAAC;GACvH,OAAO;EACT;EACA,MAAM,SAAS,cAAc,EAAE;EAC/B,OAAO,WAAW,QAAQ,OAAO,SAAS,YAAY,MAAM;CAC9D;;CAGA,SAAS,aAAgC;EACvC,OAAO,OAAO,QAAQ,MAAM,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC,IAAI,WAAW;GAC9D,MAAM,QAAQ,QAAQ,IAAI,EAAE;GAC5B,OAAO,SAAS,CAAC,MAAM,WAAW,CAAC;IAAE;IAAI,GAAG;IAAO,SAAS,MAAM;GAAQ,CAAC,IAAI,CAAC;EAClF,CAAC;CACH;CAEA,SAAS,aAAa,KAAwB;EAC5C,MAAM,EAAE,QAAQ,MAAM,IAAI;EAC1B,IAAI,CAAC,IAAI,MAAK,OAAM,IAAI,OAAO,IAAI,GAAG,CAAC,YAAY,SAAS,GAAG;EAC/D,MAAM,OAAO,EAAE,GAAG,IAAI;EACtB,KAAK,MAAM,MAAM,KACf,IAAI,KAAK,OAAO,KAAK,GAAG,CAAC,YAAY,WAAW,KAAK,MAAM;GAAE,GAAG,KAAK;GAAK,UAAU;EAAK;EAE3F,MAAM,IAAI;CACZ;CAEA,SAAS,WAA8B;EACrC,MAAM,QAAQ,WAAW;EACzB,aAAa,MAAM,KAAK,EAAE,SAAS,EAAE,CAAC;EACtC,OAAO;CACT;CAEA,SAAS,QAAQ;EACf,QAAQ,MAAM;EACd,KAAK,MAAM;EACX,MAAM,YAAY;CACpB;CAEA,SAAS,IAAI,QAAgB;EAC3B,IAAI,CAAC,OAAO,aAAa,KAAK,MAAM,UAAU,CAAC,GAAG,IAAI,GAAG,OAAO,OAAO;EACvE,MAAM;CACR;CAEA,OAAO;EAAE,OAAO;EAAO;EAAS;EAAQ;EAAQ;EAAU;EAAY;EAAc,cAAc;EAAQ,YAAY,UAAU;GAAE,UAAU;EAAM;EAAG;EAAO;CAAI;AAClK"}
1
+ {"version":3,"file":"agent-op-review.js","names":[],"sources":["../../../src/features/ai/agent-op-review.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { releaseRanges } from '../../core'\nimport type { Notifier } from '../../core/notify'\nimport { createStore, type ReadonlyStore } from '../../core/store'\nimport type { Translate } from '../../i18n'\nimport { type AgentScope, agentOpChange, agentOpSnippet } from './agent-ops'\nimport { type AiReviewChange, type AiReviewDecision, type AiReviewHandle, type AiReviewOutcome, acceptAiReview, aiReviewKey, rejectAiReview, setActiveAiReviewHunk, startAiReview } from './ai-review'\nimport { type AgentOp, type AgentOpFeedback, type AgentOpOutcome, agentOpTarget } from './protocol'\n\n/** Client state of a proposed operation: its outcome so far, the target's text, its review hunks. */\nexport interface AgentOpState {\n outcome: AgentOpOutcome\n snippet: string\n hunks: string[]\n decided: Record<AiReviewOutcome, number>\n /** Its final outcome went to the model with a user message. */\n reported: boolean\n}\n\n/** Snapshot of the proposed operations; every change replaces `ops` (never mutated in place). */\nexport interface AgentOpReviewState {\n /** Operation states by operation id. */\n ops: Readonly<Record<string, AgentOpState>>\n}\n\nexport interface AgentOpReviewOptions {\n /** Receives the notice of a decision that could not be applied because the text changed underneath. */\n notify: Notifier\n /** The app's translator (`EditorContext.t`): that notice and the snippets of the operations' target blocks. */\n t: Translate\n}\n\nexport interface AgentOpReview {\n state: ReadonlyStore<AgentOpReviewState>\n /**\n * Opens the in-document review of `ops`: every operation is checked against `scope` (the selected blocks; null:\n * the whole document) and the document as it is now. Returns the outcome of each operation, in order: `pending`\n * while it waits for the user's decision, otherwise why nothing is shown (`blocked`, `missing`, `stale`, `unchanged`,\n * `overlap`, `empty`). Operation IDs (`op`) must be unique within this review.\n */\n propose: (editor: Editor, ops: AgentOp[], scope: AgentScope | null) => AgentOpOutcome[]\n /** Accepts or rejects the open hunks of operation `id`. */\n decide: (editor: Editor, id: string, accept: boolean) => void\n /** Scrolls to operation `op`: its first open hunk (selected for accept/reject), else its target block. */\n reveal: (editor: Editor, op: AgentOp) => boolean\n /**\n * What the user decided since the last call: every operation whose outcome the model has not heard yet (open ones\n * too, as `pending`). Operations with a final outcome count as reported afterwards, so the next call omits them.\n */\n feedback: () => AgentOpFeedback[]\n /** Like `feedback`, but reports nothing as heard: for a transport that marks the operations only once its message went out (`markReported`). */\n unreported: () => AgentOpFeedback[]\n /** Marks the operations `ids` as heard by the model; unknown IDs and operations that are still pending are left alone. */\n markReported: (ids: readonly string[]) => void\n /** The number the next operation ID of a conversation-wide sequence (`op1`, `op2`, …) starts at; never reset. */\n nextOp: () => number\n /** Advances `nextOp` past `count` operations that were given IDs. */\n advanceOp: (count: number) => void\n /** Forgets every operation state and feedback; reviews still open in an editor stay open and their decisions are ignored from now on. */\n reset: () => void\n /** Rejects the reviews still open in `editor` that this one started, then forgets everything (`reset`). */\n end: (editor: Editor) => void\n}\n\n/** The state without operations; shared so an emptied review and an emptied chat hold the identical object. */\nexport const NO_AGENT_OPS: AgentOpReviewState['ops'] = Object.freeze({})\n\n/**\n * The review of agent edit operations, framework free: the chat's own transport and a host's (an agent working\n * elsewhere that proposes edits through an action) drive it the same way. `propose` shows the operations as in-document\n * hunks the user accepts or rejects (AiReviewBar); `state` follows the decisions; `feedback` reads them back.\n * Client only: needs the `AiReview` extension in the editor.\n */\nexport function createAgentOpReview({ notify, t }: AgentOpReviewOptions): AgentOpReview {\n const store = createStore<AgentOpReviewState>({ ops: NO_AGENT_OPS })\n // What `AgentOpFeedback` names beyond the state: the operation's type and target block.\n const targets = new Map<string, Pick<AgentOpFeedback, 'type' | 'target'>>()\n const open = new Set<AiReviewHandle>()\n // Never reset, so operation IDs stay unique even for reviews still open from an earlier conversation.\n let nextOp = 1\n\n const patch = (ops: Readonly<Record<string, AgentOpState>>) => store.set({ ops })\n\n function onDecision(decisions: AiReviewDecision[]) {\n const ops = { ...store.get().ops }\n for (const { changeId, outcome } of decisions) {\n const current = ops[changeId]\n if (!current) continue // from a conversation that was reset\n const decided = { ...current.decided, [outcome]: current.decided[outcome] + 1 }\n const { accepted, rejected, invalid } = decided\n const state: AgentOpState = { ...current, decided }\n if (accepted + rejected + invalid >= state.hunks.length) {\n const kinds = (['accepted', 'rejected', 'invalid'] as const).filter(kind => decided[kind] > 0)\n state.outcome = kinds.length === 1 ? kinds[0]! : 'partial'\n }\n ops[changeId] = state\n if (outcome === 'invalid') notify({ id: 'ai-review-invalid', title: t('ai.op.invalid.title'), description: t('ai.op.invalid.description'), kind: 'info' })\n }\n patch(ops)\n }\n\n function propose(editor: Editor, proposed: AgentOp[], scope: AgentScope | null): AgentOpOutcome[] {\n const allowed = scope ? new Set(scope.ids) : null\n const changes: AiReviewChange[] = []\n const states: Record<string, AgentOpState> = {}\n for (const op of proposed) {\n const change = agentOpChange(editor, op, allowed)\n states[op.op] = {\n outcome: typeof change === 'string' ? change : 'pending',\n snippet: agentOpSnippet(editor.state.doc, op, t),\n hunks: [],\n decided: { accepted: 0, rejected: 0, invalid: 0 },\n reported: false,\n }\n targets.set(op.op, { type: op.type, target: agentOpTarget(op) })\n if (typeof change !== 'string') changes.push(change)\n }\n // The states are published before the review starts: it may report decisions synchronously.\n patch({ ...store.get().ops, ...states })\n if (changes.length) {\n let started: AiReviewHandle | undefined\n const review = startAiReview(editor, changes, {\n onDecision,\n onDone: () => { if (started) open.delete(started) },\n })\n started = review\n if (review.hunks) open.add(review)\n // The review keeps anchors of its own.\n releaseRanges(editor, changes.map(c => c.kind === 'insert' ? c.at : c.range))\n const ops = { ...store.get().ops }\n for (const { id, reason } of review.skipped) {\n if (ops[id]) ops[id] = { ...ops[id], outcome: reason }\n }\n for (const hunk of aiReviewKey.getState(editor.state)?.hunks ?? []) {\n const state = hunk.reviewId === review.id ? ops[hunk.changeId] : undefined\n if (state) ops[hunk.changeId] = { ...state, hunks: [...state.hunks, hunk.id] }\n }\n patch(ops)\n }\n const { ops } = store.get()\n return proposed.map(op => ops[op.op]!.outcome)\n }\n\n function decide(editor: Editor, id: string, accept: boolean) {\n const hunks = store.get().ops[id]?.hunks\n if (!hunks?.length) return\n if (accept) acceptAiReview(editor, hunks)\n else rejectAiReview(editor, hunks)\n }\n\n function reveal(editor: Editor, op: AgentOp): boolean {\n const hunks = store.get().ops[op.op]?.hunks\n const pending = aiReviewKey.getState(editor.state)?.hunks.find(hunk => hunks?.includes(hunk.id))\n if (pending) {\n setActiveAiReviewHunk(editor.view, pending.id)\n editor.view.dom.querySelector(`[data-ai-hunk=\"${pending.id}\"]`)?.scrollIntoView({ block: 'center', behavior: 'smooth' })\n return true\n }\n const target = agentOpTarget(op)\n return target !== null && editor.commands.revealBlock(target)\n }\n\n /** Operations whose final outcome the model has not heard yet (open ones too, as „noch nicht entschieden“), in proposal order. */\n function unreported(): AgentOpFeedback[] {\n return Object.entries(store.get().ops).flatMap(([op, state]) => {\n const known = targets.get(op)\n return known && !state.reported ? [{ op, ...known, outcome: state.outcome }] : []\n })\n }\n\n function markReported(ids: readonly string[]) {\n const { ops } = store.get()\n if (!ids.some(id => ops[id] && ops[id].outcome !== 'pending')) return\n const next = { ...ops }\n for (const id of ids) {\n if (next[id] && next[id].outcome !== 'pending') next[id] = { ...next[id], reported: true }\n }\n patch(next)\n }\n\n function feedback(): AgentOpFeedback[] {\n const heard = unreported()\n markReported(heard.map(({ op }) => op))\n return heard\n }\n\n function reset() {\n targets.clear()\n open.clear()\n patch(NO_AGENT_OPS)\n }\n\n function end(editor: Editor) {\n if (!editor.isDestroyed) for (const review of [...open]) review.cancel()\n reset()\n }\n\n return { state: store, propose, decide, reveal, feedback, unreported, markReported, nextOp: () => nextOp, advanceOp: (count) => { nextOp += count }, reset, end }\n}\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import "./ai-review.js";
2
1
  import "./protocol/index.js";
2
+ import "./ai-review.js";
3
3
  import { Editor } from "@tiptap/core";
4
4
  import { EditorState } from "@tiptap/pm/state";
5
5
  import { Node as Node$1 } from "@tiptap/pm/model";
@@ -5,6 +5,7 @@ import { conversionsOf, parseBlocks } from "../../core/pipeline.js";
5
5
  import { blockConversion, parseConversionTarget } from "../../core/block-convert.js";
6
6
  import { captureRange } from "../../core/doc-anchors.js";
7
7
  import { selectionRange } from "../../core/selection-range.js";
8
+ import { stripListMarkerEcho } from "./protocol/block-lines.js";
8
9
  import { agentOpTarget } from "./protocol/agent-ops.js";
9
10
  import { Fragment } from "@tiptap/pm/model";
10
11
  //#region src/features/ai/agent-ops.ts
@@ -60,6 +61,10 @@ const AGENT_OP_OUTCOME_KEYS = {
60
61
  badge: "ai.opBadge.missing",
61
62
  hover: "ai.opTitle.missing"
62
63
  },
64
+ stale: {
65
+ badge: "ai.opBadge.stale",
66
+ hover: "ai.opTitle.stale"
67
+ },
63
68
  overlap: {
64
69
  badge: "ai.opBadge.overlap",
65
70
  hover: "ai.opTitle.overlap"
@@ -90,12 +95,10 @@ function agentOpSnippet(doc, op, t) {
90
95
  if (!text) return t(MEDIA_BLOCK_KEYS[node.type.name] ?? "ai.op.snippet.emptyBlock");
91
96
  return text.length > 60 ? `${text.slice(0, 60)}…` : text;
92
97
  }
93
- /** List and quote markers the model may echo from the document lines (`- `, `1. `, `> `) at the start of each line. */
94
- const LINE_PREFIX = /^(?:[ \t]*(?:[-*+]|\d+[.)])[ \t]+|[ \t]*>[ \t]?)+/gm;
95
98
  /**
96
99
  * The review change of an operation, re-checked against the scope the request was sent with (`allowed`, null: whole
97
- * document): 'blocked' when it points outside, 'missing' when its block is gone, 'unchanged' for a conversion that
98
- * changes nothing or is impossible there.
100
+ * document): 'blocked' when it points outside, 'stale' when its block is gone (deleted in the browser while the model was
101
+ * answering), 'unchanged' for a conversion that changes nothing or is impossible there.
99
102
  * - update: a single paragraph, or a single block of the same type and attributes (a speaker line of the same
100
103
  * speaker), is diffed within the block's content, so the block keeps its type and speaker; anything else (another
101
104
  * speaker's `**Name:**`, several blocks, a different block type) replaces the whole block. On a composite, a
@@ -121,7 +124,7 @@ function agentOpChange(editor, op, allowed) {
121
124
  markdown: op.markdown
122
125
  };
123
126
  const found = target === null ? null : findBlockById(doc, target);
124
- if (!found) return "missing";
127
+ if (!found) return "stale";
125
128
  const { node, pos } = found;
126
129
  const $pos = doc.resolve(pos);
127
130
  if (op.type === "add") {
@@ -208,7 +211,7 @@ function agentOpChange(editor, op, allowed) {
208
211
  };
209
212
  let marked = false;
210
213
  for (let depth = $pos.depth; depth > 0 && !marked; depth--) marked = ["listItem", "blockquote"].includes($pos.node(depth).type.name);
211
- const markdown = marked ? op.markdown.replace(LINE_PREFIX, "") : op.markdown;
214
+ const markdown = marked ? stripListMarkerEcho(op.markdown) : op.markdown;
212
215
  const { blocks } = parseBlocks(editor, markdown);
213
216
  const result = blocks.length === 1 ? schema.nodeFromJSON(blocks[0]) : null;
214
217
  const sameKind = result?.type === node.type && Object.keys(node.attrs).every((name) => name === "id" || result.attrs[name] === node.attrs[name]);
@@ -1 +1 @@
1
- {"version":3,"file":"agent-ops.js","names":[],"sources":["../../../src/features/ai/agent-ops.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { Fragment, type Node as PMNode } from '@tiptap/pm/model'\nimport type { EditorState } from '@tiptap/pm/state'\nimport { blockConversion, captureRange, conversionsOf, isWrapperBlock, parseBlocks, parseConversionTarget, selectionRange } from '../../core'\nimport { BLOCK_ID_ATTR, blockIdOf, findBlockById, isBlockContainer, isChromeNode } from '../../core/schema'\nimport type { MessageKey, Translate } from '../../i18n'\nimport type { AiReviewChange } from './ai-review'\nimport { type AgentOp, type AgentOpOutcome, agentOpTarget } from './protocol'\n\n/** Edit scope of a non-empty selection: the top-level blocks it touches, and every block ID inside them. */\nexport interface AgentScope { blocks: number, ids: string[] }\n\n/** The scope chat edits are limited to: the top-level blocks the selection touches; null without a selection (whole document). */\nexport function selectionScope(state: EditorState): AgentScope | null {\n if (state.selection.empty) return null\n const { from, to } = selectionRange(state)\n const ids: string[] = []\n let blocks = 0\n state.doc.forEach((node, offset) => {\n if (offset >= to || offset + node.nodeSize <= from) return\n blocks++\n const own = blockIdOf(node)\n if (own) ids.push(own)\n node.descendants((child) => {\n const id = blockIdOf(child)\n if (id) ids.push(id)\n return !child.isTextblock\n })\n })\n return { blocks, ids }\n}\n\n/** Catalog keys of how the chat shows an outcome: the badge label and the badge's hover text. */\nexport const AGENT_OP_OUTCOME_KEYS: Record<AgentOpOutcome, { badge: MessageKey, hover: MessageKey }> = {\n pending: { badge: 'ai.opBadge.pending', hover: 'ai.opTitle.pending' },\n accepted: { badge: 'ai.opBadge.accepted', hover: 'ai.opTitle.accepted' },\n rejected: { badge: 'ai.opBadge.rejected', hover: 'ai.opTitle.rejected' },\n partial: { badge: 'ai.opBadge.partial', hover: 'ai.opTitle.partial' },\n invalid: { badge: 'ai.opBadge.invalid', hover: 'ai.opTitle.invalid' },\n unchanged: { badge: 'ai.opBadge.unchanged', hover: 'ai.opTitle.unchanged' },\n missing: { badge: 'ai.opBadge.missing', hover: 'ai.opTitle.missing' },\n overlap: { badge: 'ai.opBadge.overlap', hover: 'ai.opTitle.overlap' },\n empty: { badge: 'ai.opBadge.empty', hover: 'ai.opTitle.empty' },\n blocked: { badge: 'ai.opBadge.blocked', hover: 'ai.opTitle.blocked' },\n}\n\n/** Catalog keys of the snippets of text-less media blocks. */\nconst MEDIA_BLOCK_KEYS: Record<string, MessageKey> = { image: 'ai.op.snippet.image', audio: 'ai.op.snippet.audio', video: 'ai.op.snippet.video', file: 'ai.op.snippet.file' }\n\n/** Short text of the block an operation targets, for the chat („Dokumentanfang“ for an insertion at the start). */\nexport function agentOpSnippet(doc: PMNode, op: AgentOp, t: Translate): string {\n const target = agentOpTarget(op)\n if (target === null) return t('ai.op.snippet.documentStart')\n const node = findBlockById(doc, target)?.node\n if (!node) return t('ai.op.snippet.missing')\n const text = node.textContent.replace(/\\s+/g, ' ').trim()\n if (!text) return t(MEDIA_BLOCK_KEYS[node.type.name] ?? 'ai.op.snippet.emptyBlock')\n return text.length > 60 ? `${text.slice(0, 60)}…` : text\n}\n\n/** List and quote markers the model may echo from the document lines (`- `, `1. `, `> `) at the start of each line. */\nconst LINE_PREFIX = /^(?:[ \\t]*(?:[-*+]|\\d+[.)])[ \\t]+|[ \\t]*>[ \\t]?)+/gm\n\n/**\n * The review change of an operation, re-checked against the scope the request was sent with (`allowed`, null: whole\n * document): 'blocked' when it points outside, 'missing' when its block is gone, 'unchanged' for a conversion that\n * changes nothing or is impossible there.\n * - update: a single paragraph, or a single block of the same type and attributes (a speaker line of the same\n * speaker), is diffed within the block's content, so the block keeps its type and speaker; anything else (another\n * speaker's `**Name:**`, several blocks, a different block type) replaces the whole block. On a composite, a\n * `::: kind` fence (exactly one composite) replaces the whole composite, anything else only its body.\n * - delete: the block, or the smallest list item, list or quote around it that would be invalid without it.\n * - add: after the target's block in the document, in a composite body or in the body of a host block holding blocks\n * (a callout); a list of the same type after a list item goes after that item, a host block of the same type after\n * one of its body blocks after that whole block.\n * - convert: the block (a heading becoming a composite with its group) replaced by `convertBlockTo`'s result.\n */\nexport function agentOpChange(editor: Editor, op: AgentOp, allowed: ReadonlySet<string> | null): AiReviewChange | 'blocked' | 'missing' | 'unchanged' {\n const target = agentOpTarget(op)\n if (allowed && (target === null || !allowed.has(target))) return 'blocked'\n const { state } = editor\n const { doc, schema } = state\n\n if (op.type === 'add' && target === null) return { id: op.op, kind: 'insert', at: captureRange(editor, { from: 0, to: 0 }), markdown: op.markdown }\n const found = target === null ? null : findBlockById(doc, target)\n if (!found) return 'missing'\n const { node, pos } = found\n const $pos = doc.resolve(pos)\n\n if (op.type === 'add') {\n const firstType = parseBlocks(editor, op.markdown).blocks[0]?.type\n // After the block around the target that sits in the document, in a composite body or in a host block that holds\n // blocks (a callout), so an insertion after a body block stays inside its wrapper, unless it adds a wrapper of the\n // same type (then after the whole wrapper). Lists and quotes insert after the whole list or quote.\n const holdsBody = (n: PMNode) => isBlockContainer(n) || isWrapperBlock(n)\n let container = $pos.depth\n while (container > 0 && !holdsBody($pos.node(container))) container--\n let at = container === $pos.depth ? pos + node.nodeSize : $pos.after(container + 1)\n if (container > 0 && isWrapperBlock($pos.node(container)) && $pos.node(container).type.name === firstType) at = $pos.after(container)\n for (let depth = $pos.depth; depth > 0; depth--) {\n if ($pos.node(depth).type.name !== 'listItem') continue\n if ($pos.node(depth - 1).type.name === firstType) at = $pos.after(depth)\n break\n }\n return { id: op.op, kind: 'insert', at: captureRange(editor, { from: at, to: at }), markdown: op.markdown }\n }\n\n if (op.type === 'delete') {\n let from = pos\n let to = pos + node.nodeSize\n for (let $from = $pos; $from.depth > 0; $from = doc.resolve(from)) {\n const parent = $from.parent\n const start = $from.start()\n if (parent.type.validContent(parent.content.cut(0, from - start).append(parent.content.cut(to - start)))) break\n from = $from.before()\n to = $from.after()\n }\n return { id: op.op, kind: 'delete', range: captureRange(editor, { from, to }) }\n }\n\n if (op.type === 'convert') {\n const providers = conversionsOf(editor)\n const to = parseConversionTarget(providers, op.to)\n const conversion = to && blockConversion(providers, state, $pos, to)\n if (!conversion) return 'unchanged'\n return { id: op.op, kind: 'blocks', range: captureRange(editor, conversion), blocks: Fragment.from(conversion.blocks) }\n }\n\n const whole = { from: pos, to: pos + node.nodeSize }\n if (isBlockContainer(node)) {\n const { blocks } = parseBlocks(editor, op.markdown)\n if (blocks.length === 1 && blocks[0]!.type === node.type.name) return { id: op.op, range: captureRange(editor, whole), markdown: op.markdown }\n let body = pos + 1\n node.forEach((child) => {\n if (isChromeNode(child)) body += child.nodeSize\n })\n return { id: op.op, range: captureRange(editor, { from: body, to: pos + node.nodeSize - 1 }), markdown: op.markdown }\n }\n if (!node.isTextblock) return { id: op.op, range: captureRange(editor, whole), markdown: op.markdown }\n // Lines of blocks in lists and quotes carry their markers, which the model may echo; composite bodies carry none (a\n // fence the model writes there keeps its `> description` and list items).\n let marked = false\n for (let depth = $pos.depth; depth > 0 && !marked; depth--) marked = ['listItem', 'blockquote'].includes($pos.node(depth).type.name)\n const markdown = marked ? op.markdown.replace(LINE_PREFIX, '') : op.markdown\n const { blocks } = parseBlocks(editor, markdown)\n const result = blocks.length === 1 ? schema.nodeFromJSON(blocks[0]!) : null\n const sameKind = result?.type === node.type && Object.keys(node.attrs).every(name => name === BLOCK_ID_ATTR || result.attrs[name] === node.attrs[name])\n const inside = node.content.size > 0 && (sameKind || result?.type.name === 'paragraph')\n const range = inside ? { from: pos + 1, to: pos + 1 + node.content.size } : whole\n return { id: op.op, range: captureRange(editor, range), markdown }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"agent-ops.js","names":[],"sources":["../../../src/features/ai/agent-ops.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport { Fragment, type Node as PMNode } from '@tiptap/pm/model'\nimport type { EditorState } from '@tiptap/pm/state'\nimport { blockConversion, captureRange, conversionsOf, isWrapperBlock, parseBlocks, parseConversionTarget, selectionRange } from '../../core'\nimport { BLOCK_ID_ATTR, blockIdOf, findBlockById, isBlockContainer, isChromeNode } from '../../core/schema'\nimport type { MessageKey, Translate } from '../../i18n'\nimport type { AiReviewChange } from './ai-review'\nimport { type AgentOp, type AgentOpOutcome, agentOpTarget, stripListMarkerEcho } from './protocol'\n\n/** Edit scope of a non-empty selection: the top-level blocks it touches, and every block ID inside them. */\nexport interface AgentScope { blocks: number, ids: string[] }\n\n/** The scope chat edits are limited to: the top-level blocks the selection touches; null without a selection (whole document). */\nexport function selectionScope(state: EditorState): AgentScope | null {\n if (state.selection.empty) return null\n const { from, to } = selectionRange(state)\n const ids: string[] = []\n let blocks = 0\n state.doc.forEach((node, offset) => {\n if (offset >= to || offset + node.nodeSize <= from) return\n blocks++\n const own = blockIdOf(node)\n if (own) ids.push(own)\n node.descendants((child) => {\n const id = blockIdOf(child)\n if (id) ids.push(id)\n return !child.isTextblock\n })\n })\n return { blocks, ids }\n}\n\n/** Catalog keys of how the chat shows an outcome: the badge label and the badge's hover text. */\nexport const AGENT_OP_OUTCOME_KEYS: Record<AgentOpOutcome, { badge: MessageKey, hover: MessageKey }> = {\n pending: { badge: 'ai.opBadge.pending', hover: 'ai.opTitle.pending' },\n accepted: { badge: 'ai.opBadge.accepted', hover: 'ai.opTitle.accepted' },\n rejected: { badge: 'ai.opBadge.rejected', hover: 'ai.opTitle.rejected' },\n partial: { badge: 'ai.opBadge.partial', hover: 'ai.opTitle.partial' },\n invalid: { badge: 'ai.opBadge.invalid', hover: 'ai.opTitle.invalid' },\n unchanged: { badge: 'ai.opBadge.unchanged', hover: 'ai.opTitle.unchanged' },\n missing: { badge: 'ai.opBadge.missing', hover: 'ai.opTitle.missing' },\n stale: { badge: 'ai.opBadge.stale', hover: 'ai.opTitle.stale' },\n overlap: { badge: 'ai.opBadge.overlap', hover: 'ai.opTitle.overlap' },\n empty: { badge: 'ai.opBadge.empty', hover: 'ai.opTitle.empty' },\n blocked: { badge: 'ai.opBadge.blocked', hover: 'ai.opTitle.blocked' },\n}\n\n/** Catalog keys of the snippets of text-less media blocks. */\nconst MEDIA_BLOCK_KEYS: Record<string, MessageKey> = { image: 'ai.op.snippet.image', audio: 'ai.op.snippet.audio', video: 'ai.op.snippet.video', file: 'ai.op.snippet.file' }\n\n/** Short text of the block an operation targets, for the chat („Dokumentanfang“ for an insertion at the start). */\nexport function agentOpSnippet(doc: PMNode, op: AgentOp, t: Translate): string {\n const target = agentOpTarget(op)\n if (target === null) return t('ai.op.snippet.documentStart')\n const node = findBlockById(doc, target)?.node\n if (!node) return t('ai.op.snippet.missing')\n const text = node.textContent.replace(/\\s+/g, ' ').trim()\n if (!text) return t(MEDIA_BLOCK_KEYS[node.type.name] ?? 'ai.op.snippet.emptyBlock')\n return text.length > 60 ? `${text.slice(0, 60)}…` : text\n}\n\n/**\n * The review change of an operation, re-checked against the scope the request was sent with (`allowed`, null: whole\n * document): 'blocked' when it points outside, 'stale' when its block is gone (deleted in the browser while the model was\n * answering), 'unchanged' for a conversion that changes nothing or is impossible there.\n * - update: a single paragraph, or a single block of the same type and attributes (a speaker line of the same\n * speaker), is diffed within the block's content, so the block keeps its type and speaker; anything else (another\n * speaker's `**Name:**`, several blocks, a different block type) replaces the whole block. On a composite, a\n * `::: kind` fence (exactly one composite) replaces the whole composite, anything else only its body.\n * - delete: the block, or the smallest list item, list or quote around it that would be invalid without it.\n * - add: after the target's block in the document, in a composite body or in the body of a host block holding blocks\n * (a callout); a list of the same type after a list item goes after that item, a host block of the same type after\n * one of its body blocks after that whole block.\n * - convert: the block (a heading becoming a composite with its group) replaced by `convertBlockTo`'s result.\n */\nexport function agentOpChange(editor: Editor, op: AgentOp, allowed: ReadonlySet<string> | null): AiReviewChange | 'blocked' | 'stale' | 'unchanged' {\n const target = agentOpTarget(op)\n if (allowed && (target === null || !allowed.has(target))) return 'blocked'\n const { state } = editor\n const { doc, schema } = state\n\n if (op.type === 'add' && target === null) return { id: op.op, kind: 'insert', at: captureRange(editor, { from: 0, to: 0 }), markdown: op.markdown }\n const found = target === null ? null : findBlockById(doc, target)\n if (!found) return 'stale'\n const { node, pos } = found\n const $pos = doc.resolve(pos)\n\n if (op.type === 'add') {\n const firstType = parseBlocks(editor, op.markdown).blocks[0]?.type\n // After the block around the target that sits in the document, in a composite body or in a host block that holds\n // blocks (a callout), so an insertion after a body block stays inside its wrapper, unless it adds a wrapper of the\n // same type (then after the whole wrapper). Lists and quotes insert after the whole list or quote.\n const holdsBody = (n: PMNode) => isBlockContainer(n) || isWrapperBlock(n)\n let container = $pos.depth\n while (container > 0 && !holdsBody($pos.node(container))) container--\n let at = container === $pos.depth ? pos + node.nodeSize : $pos.after(container + 1)\n if (container > 0 && isWrapperBlock($pos.node(container)) && $pos.node(container).type.name === firstType) at = $pos.after(container)\n for (let depth = $pos.depth; depth > 0; depth--) {\n if ($pos.node(depth).type.name !== 'listItem') continue\n if ($pos.node(depth - 1).type.name === firstType) at = $pos.after(depth)\n break\n }\n return { id: op.op, kind: 'insert', at: captureRange(editor, { from: at, to: at }), markdown: op.markdown }\n }\n\n if (op.type === 'delete') {\n let from = pos\n let to = pos + node.nodeSize\n for (let $from = $pos; $from.depth > 0; $from = doc.resolve(from)) {\n const parent = $from.parent\n const start = $from.start()\n if (parent.type.validContent(parent.content.cut(0, from - start).append(parent.content.cut(to - start)))) break\n from = $from.before()\n to = $from.after()\n }\n return { id: op.op, kind: 'delete', range: captureRange(editor, { from, to }) }\n }\n\n if (op.type === 'convert') {\n const providers = conversionsOf(editor)\n const to = parseConversionTarget(providers, op.to)\n const conversion = to && blockConversion(providers, state, $pos, to)\n if (!conversion) return 'unchanged'\n return { id: op.op, kind: 'blocks', range: captureRange(editor, conversion), blocks: Fragment.from(conversion.blocks) }\n }\n\n const whole = { from: pos, to: pos + node.nodeSize }\n if (isBlockContainer(node)) {\n const { blocks } = parseBlocks(editor, op.markdown)\n if (blocks.length === 1 && blocks[0]!.type === node.type.name) return { id: op.op, range: captureRange(editor, whole), markdown: op.markdown }\n let body = pos + 1\n node.forEach((child) => {\n if (isChromeNode(child)) body += child.nodeSize\n })\n return { id: op.op, range: captureRange(editor, { from: body, to: pos + node.nodeSize - 1 }), markdown: op.markdown }\n }\n if (!node.isTextblock) return { id: op.op, range: captureRange(editor, whole), markdown: op.markdown }\n // Lines of blocks in lists and quotes carry their markers, which the model may echo; composite bodies carry none (a\n // fence the model writes there keeps its `> description` and list items).\n let marked = false\n for (let depth = $pos.depth; depth > 0 && !marked; depth--) marked = ['listItem', 'blockquote'].includes($pos.node(depth).type.name)\n const markdown = marked ? stripListMarkerEcho(op.markdown) : op.markdown\n const { blocks } = parseBlocks(editor, markdown)\n const result = blocks.length === 1 ? schema.nodeFromJSON(blocks[0]!) : null\n const sameKind = result?.type === node.type && Object.keys(node.attrs).every(name => name === BLOCK_ID_ATTR || result.attrs[name] === node.attrs[name])\n const inside = node.content.size > 0 && (sameKind || result?.type.name === 'paragraph')\n const range = inside ? { from: pos + 1, to: pos + 1 + node.content.size } : whole\n return { id: op.op, range: captureRange(editor, range), markdown }\n}\n"],"mappings":"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"}
@@ -2,9 +2,9 @@ import { Translate } from "../../i18n/index.js";
2
2
  import { Notice } from "../../core/notify.js";
3
3
  import { DocAnchor } from "../../core/doc-anchors.js";
4
4
  import "../../core/index.js";
5
- import { AiReviewSkipReason } from "./ai-review.js";
6
5
  import { AiApplyMode, AiTask } from "./protocol/tasks.js";
7
6
  import "./protocol/index.js";
7
+ import { AiReviewSkipReason } from "./ai-review.js";
8
8
  import { Editor, JSONContent } from "@tiptap/core";
9
9
  import { Transaction } from "@tiptap/pm/state";
10
10
  import { Fragment, ResolvedPos } from "@tiptap/pm/model";
@@ -214,6 +214,12 @@ function acceptAiReview(editor, ids) {
214
214
  const chosen = aiReviewKey.getState(editor.state)?.hunks.filter((h) => !ids || ids.includes(h.id)) ?? [];
215
215
  if (!chosen.length) return;
216
216
  const tr = editor.state.tr;
217
+ const pending = aiReviewKey.getState(editor.state)?.hunks ?? [];
218
+ const after = pending.filter((hunk) => {
219
+ if (hunk.kind !== "insert" || chosen.includes(hunk)) return false;
220
+ const written = chosen.filter((h) => h.kind === "insert" && h.from === hunk.from).map((h) => pending.indexOf(h));
221
+ return written.length > 0 && written.every((index) => index < pending.indexOf(hunk));
222
+ }).map((hunk) => hunk.anchor.id);
217
223
  for (const hunk of [...chosen].reverse().sort((a, b) => b.from - a.from || b.to - a.to)) if (hunk.kind === "block") replaceRangeWithBlocks(tr, hunk.from, hunk.to, hunk.insert);
218
224
  else tr.replaceWith(hunk.from, hunk.to, hunk.insert);
219
225
  tr.setMeta(aiReviewKey, {
@@ -221,7 +227,10 @@ function acceptAiReview(editor, ids) {
221
227
  ids: chosen.map((h) => h.id),
222
228
  outcome: "accepted"
223
229
  });
224
- tr.setMeta(docAnchorsKey, { remove: chosen.map((h) => h.anchor.id) });
230
+ tr.setMeta(docAnchorsKey, {
231
+ remove: chosen.map((h) => h.anchor.id),
232
+ after
233
+ });
225
234
  const resolver = anchorResolverOf(editor);
226
235
  const collaborative = resolver?.stopUndoCapture(editor.state) ?? false;
227
236
  editor.view.dispatch(closeHistory(tr));