@skaile/forge-editor 5.0.0 → 5.0.2
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.
- package/CHANGELOG.md +17 -0
- package/dist/code/CodeSurface.vue.d.ts +2 -2
- package/dist/document/DocumentSurface.vue.d.ts +2 -2
- package/dist/features/ai/actions.d.ts +4 -2
- package/dist/features/ai/actions.js +23 -11
- package/dist/features/ai/actions.js.map +1 -1
- package/dist/features/ai/document-chat-model.d.ts +9 -2
- package/dist/features/ai/document-chat-model.js +23 -8
- package/dist/features/ai/document-chat-model.js.map +1 -1
- package/dist/features/media/index.js +1 -1
- package/dist/features/media/react/AudioRecorderModal.js +1 -1
- package/dist/features/media/react.js +1 -1
- package/dist/integrations/composite-ai/transfer-transforms.js +1 -1
- package/dist/markdown/MarkdownSurface.vue.d.ts +4 -4
- package/dist/presets/compose.js +1 -1
- package/dist/presets/layer.js +1 -1
- package/dist/tree/tree-host.js +1 -1
- package/dist/vue/AdvancedEditor.vue.d.ts +4 -4
- package/dist/vue/SimpleEditor.vue.d.ts +4 -4
- package/dist/vue/components/EditorSurface.vue.d.ts +4 -4
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 5.0.2 - 2026-10-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `DocumentChatOptions.review`: the review the document chat proposes to (default: its own). A host that also runs `documentActions` passes the same `AgentOpReview` to both, so the operation IDs come from one counter and `reset()` forgets both sets of proposals.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Operation IDs of the document chat could collide with those of `documentActions` on a shared review: the chat took its starting number when the request went out and advanced the counter only after the stream ended, so an answer arriving after a `doc.propose_ops` call overwrote that call's `op<n>` state and target. The chat now renumbers the answer's operations from the review's counter when the `ops` event arrives (past the IDs of the change set, which keeps the server's), and advances the counter then.
|
|
12
|
+
- `doc.propose_ops` result: `outcomes` is one `{ op, type, target, outcome }` per valid operation (it was a bare list of outcome codes, without the `op<n>` IDs and ordered only by the valid operations, so the model could not tell which of its operations was meant), and `proposed` counts only the operations waiting for the user (`pending`), not every valid one. With nothing pending the `hint` no longer tells the model the user reviews proposals. The input schema is as loose as the server's `edit_document` tool (`type` a string, no `enum`, no count or length limit), so a bad `type`, a 51st operation or a Markdown that is only too long because of an echoed `[id] ` prefix is a problem of that operation from `checkAgentOps` and the other operations of the call still go through.
|
|
13
|
+
|
|
14
|
+
## 5.0.1 - 2026-10-26
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Optional peer `@skaile/forge-app-actions` accepts `^0.1.0 || ^0.2.0`. A caret on 0.x excludes the next minor, so a host on forge-app-actions 0.2 (the `./chat` binding that turns `documentActions` into chat tools) got a peer warning and a second copy.
|
|
19
|
+
|
|
3
20
|
## 5.0.0 - 2026-10-26
|
|
4
21
|
|
|
5
22
|
### Added
|
|
@@ -11,13 +11,13 @@ type __VLS_Props = {
|
|
|
11
11
|
declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {
|
|
12
12
|
view: import("vue").ShallowRef<EditorView | undefined, EditorView | undefined>;
|
|
13
13
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
14
|
-
ready: (view: EditorView) => any;
|
|
15
14
|
"update:modelValue": (text: string) => any;
|
|
16
15
|
save: (text: string) => any;
|
|
16
|
+
ready: (view: EditorView) => any;
|
|
17
17
|
}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
18
|
-
onReady?: ((view: EditorView) => any) | undefined;
|
|
19
18
|
"onUpdate:modelValue"?: ((text: string) => any) | undefined;
|
|
20
19
|
onSave?: ((text: string) => any) | undefined;
|
|
20
|
+
onReady?: ((view: EditorView) => any) | undefined;
|
|
21
21
|
}>, {
|
|
22
22
|
modelValue: string;
|
|
23
23
|
editable: boolean;
|
|
@@ -32,13 +32,13 @@ 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;
|
|
37
36
|
save: (text: string) => any;
|
|
37
|
+
ready: (target: DocumentSurfaceTarget) => 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;
|
|
41
40
|
onSave?: ((text: string) => any) | undefined;
|
|
41
|
+
onReady?: ((target: DocumentSurfaceTarget) => any) | undefined;
|
|
42
42
|
}>, {
|
|
43
43
|
modelValue: string;
|
|
44
44
|
editable: boolean;
|
|
@@ -25,8 +25,10 @@ export interface DocumentActionsOptions {
|
|
|
25
25
|
* - `doc.propose_ops` checks the operations against the block IDs of that text, the `scope` and the `targets`
|
|
26
26
|
* (`checkAgentOps`), gives the valid ones `op<n>` IDs from `review.nextOp()` and shows them as review hunks
|
|
27
27
|
* (`review.propose`). It never throws on a bad operation: the result lists the `problems` (`unknown`, `scope`, `field`,
|
|
28
|
-
* `duplicate`,
|
|
29
|
-
*
|
|
28
|
+
* `limit`, `duplicate`, …; `index` is the 1-based position in the input) and the model sends corrected operations in its
|
|
29
|
+
* next call. The result's `outcomes` has one `{ op, type, target, outcome }` per valid operation, in input order (`op` is
|
|
30
|
+
* its `op<n>` ID, `outcome` `pending` while it waits for the user, else why nothing is shown), and `proposed` counts the
|
|
31
|
+
* `pending` ones. No approval dialog: the in-document review is the approval.
|
|
30
32
|
*/
|
|
31
33
|
export declare function documentActions({ editor, review, scope, targets }: DocumentActionsOptions): ForgeAction[];
|
|
32
34
|
//#endregion
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AGENT_OP_TYPES } from "../../core/agent-op-types.js";
|
|
2
2
|
import { readBlockLines } from "./protocol/block-lines.js";
|
|
3
|
-
import {
|
|
3
|
+
import { agentOpTarget, checkAgentOps } from "./protocol/agent-ops.js";
|
|
4
4
|
import { documentChatText } from "./document-chat.js";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { defineForgeAction } from "@skaile/forge-app-actions/client-capabilities";
|
|
@@ -15,14 +15,18 @@ import { defineForgeAction } from "@skaile/forge-app-actions/client-capabilities
|
|
|
15
15
|
*/
|
|
16
16
|
const READ_DESCRIPTION = "Liefert das aktuelle Dokument, eine Zeile pro Block, beginnend mit der Block-ID.";
|
|
17
17
|
const PROPOSE_DESCRIPTION = "Schlägt Änderungen am Dokument vor. Jede Operation wird der Nutzerin bzw. dem Nutzer im Dokument zur Prüfung angezeigt.";
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
/**
|
|
19
|
+
* The operation list of `doc.propose_ops`: the shape of the editor's `edit_document` tool, and as loose as the server's
|
|
20
|
+
* (`agentTools` in `server/handlers/ai.ts`): the type, the limits and the block rules (IDs, scope, targets) are not checked
|
|
21
|
+
* here but by `checkAgentOps`, so one bad operation (or a 51st) becomes a problem of its own, not a refusal of the whole call.
|
|
22
|
+
*/
|
|
23
|
+
const proposeInput = z.object({ ops: z.array(z.looseObject({
|
|
24
|
+
type: z.string().describe(`${AGENT_OP_TYPES.join(" | ")}. update: Blockinhalt ersetzen; delete: Block löschen; add: Blöcke einfügen; convert: Blocktyp ändern.`),
|
|
21
25
|
id: z.string().optional().describe("update/delete/convert: ID des Blocks aus dem Dokument."),
|
|
22
26
|
afterId: z.string().nullable().optional().describe("add (Pflicht): ID des Blocks, nach dem eingefügt wird; für den Dokumentanfang ausdrücklich null."),
|
|
23
|
-
markdown: z.string().
|
|
27
|
+
markdown: z.string().optional().describe("update/add: der neue Inhalt als Markdown, ohne Block-ID."),
|
|
24
28
|
to: z.string().optional().describe("convert: der neue Blocktyp, einer der erlaubten Werte (z. B. paragraph, heading2, composite:task).")
|
|
25
|
-
})).
|
|
29
|
+
})).describe("Die Operationen in Dokumentreihenfolge.") });
|
|
26
30
|
/**
|
|
27
31
|
* `doc.read` and `doc.propose_ops` as forge actions (capability names `app.doc.read`, `app.doc.propose_ops`; audience
|
|
28
32
|
* `llm`, so neither shows in a command palette).
|
|
@@ -31,8 +35,10 @@ const proposeInput = z.object({ ops: z.array(z.object({
|
|
|
31
35
|
* - `doc.propose_ops` checks the operations against the block IDs of that text, the `scope` and the `targets`
|
|
32
36
|
* (`checkAgentOps`), gives the valid ones `op<n>` IDs from `review.nextOp()` and shows them as review hunks
|
|
33
37
|
* (`review.propose`). It never throws on a bad operation: the result lists the `problems` (`unknown`, `scope`, `field`,
|
|
34
|
-
* `duplicate`,
|
|
35
|
-
*
|
|
38
|
+
* `limit`, `duplicate`, …; `index` is the 1-based position in the input) and the model sends corrected operations in its
|
|
39
|
+
* next call. The result's `outcomes` has one `{ op, type, target, outcome }` per valid operation, in input order (`op` is
|
|
40
|
+
* its `op<n>` ID, `outcome` `pending` while it waits for the user, else why nothing is shown), and `proposed` counts the
|
|
41
|
+
* `pending` ones. No approval dialog: the in-document review is the approval.
|
|
36
42
|
*/
|
|
37
43
|
function documentActions({ editor, review, scope = () => null, targets = [] }) {
|
|
38
44
|
const current = () => {
|
|
@@ -68,11 +74,17 @@ function documentActions({ editor, review, scope = () => null, targets = [] }) {
|
|
|
68
74
|
}));
|
|
69
75
|
review.advanceOp(ops.length);
|
|
70
76
|
const outcomes = review.propose(instance, ops, editScope);
|
|
77
|
+
const proposed = outcomes.filter((outcome) => outcome === "pending").length;
|
|
71
78
|
return {
|
|
72
|
-
proposed
|
|
73
|
-
outcomes,
|
|
79
|
+
proposed,
|
|
80
|
+
outcomes: ops.map((op, index) => ({
|
|
81
|
+
op: op.op,
|
|
82
|
+
type: op.type,
|
|
83
|
+
target: agentOpTarget(op),
|
|
84
|
+
outcome: outcomes[index]
|
|
85
|
+
})),
|
|
74
86
|
problems: checked.problems,
|
|
75
|
-
hint: "Die Nutzerin bzw. der Nutzer prüft die Vorschläge im Dokument."
|
|
87
|
+
hint: proposed ? "Die Nutzerin bzw. der Nutzer prüft die Vorschläge im Dokument." : "Es wird nichts zur Prüfung angezeigt; prüfe die Ergebnisse und Probleme."
|
|
76
88
|
};
|
|
77
89
|
}
|
|
78
90
|
})];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"actions.js","names":[],"sources":["../../../src/features/ai/actions.ts"],"sourcesContent":["// i18n-ignore-file: model-facing tool descriptions and results (German; the model's language is not the editor's)\n/**\n * The document chat as two forge actions, for a host whose chat lives elsewhere (a Nuxt app with its own AI SDK route\n * and `@skaile/forge-app-actions/chat`): the model reads the document as block lines and proposes edit operations,\n * which the user reviews in the document. The server-side `edit_document` loop of `server/handlers/ai.ts` does the same\n * with a server-run tool; here the tool runs in the browser, next to the editor.\n *\n * Needs the optional peer `@skaile/forge-app-actions` and the `AiReview` extension in the editor.\n */\nimport { defineForgeAction, type ForgeAction } from '@skaile/forge-app-actions/client-capabilities'\nimport type { Editor } from '@tiptap/core'\nimport { z } from 'zod'\nimport type { AgentOpReview } from './agent-op-review'\nimport type { AgentScope } from './agent-ops'\nimport { documentChatText } from './document-chat'\nimport {
|
|
1
|
+
{"version":3,"file":"actions.js","names":[],"sources":["../../../src/features/ai/actions.ts"],"sourcesContent":["// i18n-ignore-file: model-facing tool descriptions and results (German; the model's language is not the editor's)\n/**\n * The document chat as two forge actions, for a host whose chat lives elsewhere (a Nuxt app with its own AI SDK route\n * and `@skaile/forge-app-actions/chat`): the model reads the document as block lines and proposes edit operations,\n * which the user reviews in the document. The server-side `edit_document` loop of `server/handlers/ai.ts` does the same\n * with a server-run tool; here the tool runs in the browser, next to the editor.\n *\n * Needs the optional peer `@skaile/forge-app-actions` and the `AiReview` extension in the editor.\n */\nimport { defineForgeAction, type ForgeAction } from '@skaile/forge-app-actions/client-capabilities'\nimport type { Editor } from '@tiptap/core'\nimport { z } from 'zod'\nimport type { AgentOpReview } from './agent-op-review'\nimport type { AgentScope } from './agent-ops'\nimport { documentChatText } from './document-chat'\nimport { AGENT_OP_TYPES, agentOpTarget, checkAgentOps, type AgentOp, readBlockLines } from './protocol'\n\nexport interface DocumentActionsOptions {\n /** The editor the actions work on, read on every call; `undefined` (not mounted yet) makes the actions fail with `no editor`. */\n editor: () => Editor | undefined\n /**\n * The review that shows the proposals (`createAgentOpReview`); its decisions stay with the host. Operation IDs\n * (`op1`, `op2`, …) come from its counter (`nextOp`/`advanceOp`), so the numbering continues across turns and across\n * every other source that proposes to the same review (the editor's own document chat).\n */\n review: AgentOpReview\n /** The edit scope of the next proposal: the selected blocks (`selectionScope(editor.state)`); `null`: the whole document. Default: the whole document. */\n scope?: () => AgentScope | null\n /** Block types `convert` may turn a block into; a `convert` to any other type is a `target` problem. Default: none. */\n targets?: readonly string[]\n}\n\nconst READ_DESCRIPTION = 'Liefert das aktuelle Dokument, eine Zeile pro Block, beginnend mit der Block-ID.'\n\nconst PROPOSE_DESCRIPTION = 'Schlägt Änderungen am Dokument vor. Jede Operation wird der Nutzerin bzw. dem Nutzer im Dokument zur Prüfung angezeigt.'\n\n/**\n * The operation list of `doc.propose_ops`: the shape of the editor's `edit_document` tool, and as loose as the server's\n * (`agentTools` in `server/handlers/ai.ts`): the type, the limits and the block rules (IDs, scope, targets) are not checked\n * here but by `checkAgentOps`, so one bad operation (or a 51st) becomes a problem of its own, not a refusal of the whole call.\n */\nconst proposeInput = z.object({\n ops: z.array(z.looseObject({\n type: z.string().describe(`${AGENT_OP_TYPES.join(' | ')}. update: Blockinhalt ersetzen; delete: Block löschen; add: Blöcke einfügen; convert: Blocktyp ändern.`),\n id: z.string().optional().describe('update/delete/convert: ID des Blocks aus dem Dokument.'),\n afterId: z.string().nullable().optional().describe('add (Pflicht): ID des Blocks, nach dem eingefügt wird; für den Dokumentanfang ausdrücklich null.'),\n markdown: z.string().optional().describe('update/add: der neue Inhalt als Markdown, ohne Block-ID.'),\n to: z.string().optional().describe('convert: der neue Blocktyp, einer der erlaubten Werte (z. B. paragraph, heading2, composite:task).'),\n })).describe('Die Operationen in Dokumentreihenfolge.'),\n})\n\n/**\n * `doc.read` and `doc.propose_ops` as forge actions (capability names `app.doc.read`, `app.doc.propose_ops`; audience\n * `llm`, so neither shows in a command palette).\n *\n * - `doc.read` returns `documentChatText(editor)`: one line per block, `[<block id>] <Markdown>`.\n * - `doc.propose_ops` checks the operations against the block IDs of that text, the `scope` and the `targets`\n * (`checkAgentOps`), gives the valid ones `op<n>` IDs from `review.nextOp()` and shows them as review hunks\n * (`review.propose`). It never throws on a bad operation: the result lists the `problems` (`unknown`, `scope`, `field`,\n * `limit`, `duplicate`, …; `index` is the 1-based position in the input) and the model sends corrected operations in its\n * next call. The result's `outcomes` has one `{ op, type, target, outcome }` per valid operation, in input order (`op` is\n * its `op<n>` ID, `outcome` `pending` while it waits for the user, else why nothing is shown), and `proposed` counts the\n * `pending` ones. No approval dialog: the in-document review is the approval.\n */\nexport function documentActions({ editor, review, scope = () => null, targets = [] }: DocumentActionsOptions): ForgeAction[] {\n const current = (): Editor => {\n const instance = editor()\n if (!instance || instance.isDestroyed) throw new Error('no editor')\n return instance\n }\n\n const read = defineForgeAction({\n id: 'doc.read',\n label: 'Dokument lesen',\n description: READ_DESCRIPTION,\n audience: ['llm'],\n run: () => documentChatText(current()),\n })\n\n const proposeOps = defineForgeAction({\n id: 'doc.propose_ops',\n label: 'Änderungen am Dokument vorschlagen',\n description: PROPOSE_DESCRIPTION,\n input: proposeInput,\n audience: ['llm'],\n requiresApproval: false,\n run: (input) => {\n const instance = current()\n const editScope = scope()\n const checked = checkAgentOps(input.ops, {\n lines: readBlockLines(documentChatText(instance)),\n allowed: editScope ? new Set(editScope.ids) : null,\n targets: new Set(targets),\n })\n const first = review.nextOp()\n const ops: AgentOp[] = checked.valid.map((op, index) => ({ ...op, op: `op${first + index}` }))\n review.advanceOp(ops.length)\n const outcomes = review.propose(instance, ops, editScope)\n const proposed = outcomes.filter(outcome => outcome === 'pending').length\n return {\n proposed,\n outcomes: ops.map((op, index) => ({ op: op.op, type: op.type, target: agentOpTarget(op), outcome: outcomes[index] })),\n problems: checked.problems,\n hint: proposed\n ? 'Die Nutzerin bzw. der Nutzer prüft die Vorschläge im Dokument.'\n : 'Es wird nichts zur Prüfung angezeigt; prüfe die Ergebnisse und Probleme.',\n }\n },\n })\n\n return [read, proposeOps]\n}\n"],"mappings":";;;;;;;;;;;;;;;AAgCA,MAAM,mBAAmB;AAEzB,MAAM,sBAAsB;;;;;;AAO5B,MAAM,eAAe,EAAE,OAAO,EAC5B,KAAK,EAAE,MAAM,EAAE,YAAY;CACzB,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS,GAAG,eAAe,KAAK,KAAK,EAAE,uGAAuG;CAC/J,IAAI,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,wDAAwD;CAC3F,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,kGAAkG;CACrJ,UAAU,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,0DAA0D;CACnG,IAAI,EAAE,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,oGAAoG;AACzI,CAAC,CAAC,CAAC,CAAC,SAAS,yCAAyC,EACxD,CAAC;;;;;;;;;;;;;;AAeD,SAAgB,gBAAgB,EAAE,QAAQ,QAAQ,cAAc,MAAM,UAAU,CAAC,KAA4C;CAC3H,MAAM,gBAAwB;EAC5B,MAAM,WAAW,OAAO;EACxB,IAAI,CAAC,YAAY,SAAS,aAAa,MAAM,IAAI,MAAM,WAAW;EAClE,OAAO;CACT;CAyCA,OAAO,CAvCM,kBAAkB;EAC7B,IAAI;EACJ,OAAO;EACP,aAAa;EACb,UAAU,CAAC,KAAK;EAChB,WAAW,iBAAiB,QAAQ,CAAC;CACvC,CAiCW,GA/BQ,kBAAkB;EACnC,IAAI;EACJ,OAAO;EACP,aAAa;EACb,OAAO;EACP,UAAU,CAAC,KAAK;EAChB,kBAAkB;EAClB,MAAM,UAAU;GACd,MAAM,WAAW,QAAQ;GACzB,MAAM,YAAY,MAAM;GACxB,MAAM,UAAU,cAAc,MAAM,KAAK;IACvC,OAAO,eAAe,iBAAiB,QAAQ,CAAC;IAChD,SAAS,YAAY,IAAI,IAAI,UAAU,GAAG,IAAI;IAC9C,SAAS,IAAI,IAAI,OAAO;GAC1B,CAAC;GACD,MAAM,QAAQ,OAAO,OAAO;GAC5B,MAAM,MAAiB,QAAQ,MAAM,KAAK,IAAI,WAAW;IAAE,GAAG;IAAI,IAAI,KAAK,QAAQ;GAAQ,EAAE;GAC7F,OAAO,UAAU,IAAI,MAAM;GAC3B,MAAM,WAAW,OAAO,QAAQ,UAAU,KAAK,SAAS;GACxD,MAAM,WAAW,SAAS,QAAO,YAAW,YAAY,SAAS,CAAC,CAAC;GACnE,OAAO;IACL;IACA,UAAU,IAAI,KAAK,IAAI,WAAW;KAAE,IAAI,GAAG;KAAI,MAAM,GAAG;KAAM,QAAQ,cAAc,EAAE;KAAG,SAAS,SAAS;IAAO,EAAE;IACpH,UAAU,QAAQ;IAClB,MAAM,WACF,mEACA;GACN;EACF;CACF,CAEuB,CAAC;AAC1B"}
|
|
@@ -12,7 +12,7 @@ import { DocumentChatMessage } from "./protocol/chat.js";
|
|
|
12
12
|
import { AiTask } from "./protocol/tasks.js";
|
|
13
13
|
import "./protocol/index.js";
|
|
14
14
|
import { AgentScope } from "./agent-ops.js";
|
|
15
|
-
import { AgentOpReviewState } from "./agent-op-review.js";
|
|
15
|
+
import { AgentOpReview, AgentOpReviewState } from "./agent-op-review.js";
|
|
16
16
|
import { AiTarget } from "./ai-apply.js";
|
|
17
17
|
import { Editor } from "@tiptap/core";
|
|
18
18
|
//#region src/features/ai/document-chat-model.d.ts
|
|
@@ -66,6 +66,13 @@ export interface DocumentChatOptions {
|
|
|
66
66
|
* Documents are still stored by the server, but nobody here shows or reports them.
|
|
67
67
|
*/
|
|
68
68
|
changeSets?: ChangeSetFeed;
|
|
69
|
+
/**
|
|
70
|
+
* The review that shows the proposed operations; default: the chat's own. Pass the one `documentActions` proposes to when a
|
|
71
|
+
* host chat and this one work on the same document: operation IDs (`op<n>`) then come from one counter, an answer's
|
|
72
|
+
* operations are renumbered when they arrive (another proposer may have taken the IDs the request was sent with), and
|
|
73
|
+
* `reset()` forgets the operations of both.
|
|
74
|
+
*/
|
|
75
|
+
review?: AgentOpReview;
|
|
69
76
|
}
|
|
70
77
|
export interface DocumentChat {
|
|
71
78
|
state: ReadonlyStore<DocumentChatState>;
|
|
@@ -94,6 +101,6 @@ export interface DocumentChat {
|
|
|
94
101
|
* message, together with the document as it is at that moment and the edit scope. Proposed edit operations open an
|
|
95
102
|
* in-document review; their decisions go to the model with the next message.
|
|
96
103
|
*/
|
|
97
|
-
export declare function createDocumentChat({ ai, modelRef, notify, t, changeSets }: DocumentChatOptions): DocumentChat;
|
|
104
|
+
export declare function createDocumentChat({ ai, modelRef, notify, t, changeSets, review }: DocumentChatOptions): DocumentChat;
|
|
98
105
|
//#endregion
|
|
99
106
|
//# sourceMappingURL=document-chat-model.d.ts.map
|
|
@@ -22,12 +22,11 @@ const EMPTY = {
|
|
|
22
22
|
* message, together with the document as it is at that moment and the edit scope. Proposed edit operations open an
|
|
23
23
|
* in-document review; their decisions go to the model with the next message.
|
|
24
24
|
*/
|
|
25
|
-
function createDocumentChat({ ai, modelRef, notify, t, changeSets
|
|
25
|
+
function createDocumentChat({ ai, modelRef, notify, t, changeSets, review = createAgentOpReview({
|
|
26
|
+
notify: silentNotifier,
|
|
27
|
+
t
|
|
28
|
+
}) }) {
|
|
26
29
|
const store = createStore(EMPTY);
|
|
27
|
-
const review = createAgentOpReview({
|
|
28
|
-
notify: silentNotifier,
|
|
29
|
-
t
|
|
30
|
-
});
|
|
31
30
|
let controller = null;
|
|
32
31
|
const patch = (changes) => store.set({
|
|
33
32
|
...store.get(),
|
|
@@ -104,12 +103,29 @@ function createDocumentChat({ ai, modelRef, notify, t, changeSets }) {
|
|
|
104
103
|
});
|
|
105
104
|
const received = {};
|
|
106
105
|
const openDocumentId = documentIdOf(editor);
|
|
106
|
+
const sentNextOp = review.nextOp();
|
|
107
|
+
const numbered = (proposal, changeSetOps) => {
|
|
108
|
+
const first = review.nextOp();
|
|
109
|
+
if (first === sentNextOp) {
|
|
110
|
+
review.advanceOp(proposal.ops.length + changeSetOps);
|
|
111
|
+
return proposal;
|
|
112
|
+
}
|
|
113
|
+
const start = Math.max(first, sentNextOp + proposal.ops.length + changeSetOps);
|
|
114
|
+
review.advanceOp(start + proposal.ops.length - first);
|
|
115
|
+
return {
|
|
116
|
+
...proposal,
|
|
117
|
+
ops: proposal.ops.map((op, index) => ({
|
|
118
|
+
...op,
|
|
119
|
+
op: `op${start + index}`
|
|
120
|
+
}))
|
|
121
|
+
};
|
|
122
|
+
};
|
|
107
123
|
const body = {
|
|
108
124
|
messages: sent,
|
|
109
125
|
document: documentChatText(editor),
|
|
110
126
|
...openDocumentId && { documentId: openDocumentId },
|
|
111
127
|
scope: scope?.ids ?? null,
|
|
112
|
-
nextOp:
|
|
128
|
+
nextOp: sentNextOp,
|
|
113
129
|
model: modelRef("generate"),
|
|
114
130
|
blockTypes,
|
|
115
131
|
blocks,
|
|
@@ -127,7 +143,7 @@ function createDocumentChat({ ai, modelRef, notify, t, changeSets }) {
|
|
|
127
143
|
},
|
|
128
144
|
onEvent: (event, data) => {
|
|
129
145
|
if (controller !== own) return;
|
|
130
|
-
if (event === "ops") received.proposal = data;
|
|
146
|
+
if (event === "ops") received.proposal = numbered(data, received.changeSet?.hunks.length ?? 0);
|
|
131
147
|
else if (event === "change-set") received.changeSet = data;
|
|
132
148
|
else if (event === "tool") answer = replaceTurn(answer, {
|
|
133
149
|
...answer,
|
|
@@ -153,7 +169,6 @@ function createDocumentChat({ ai, modelRef, notify, t, changeSets }) {
|
|
|
153
169
|
problems: dropped,
|
|
154
170
|
...changeSetOps.length && { changeSetOps }
|
|
155
171
|
});
|
|
156
|
-
review.advanceOp(proposed.length + changeSetOps.length);
|
|
157
172
|
if (proposed.length || dropped.length) {
|
|
158
173
|
if (!editor.isDestroyed) review.propose(editor, proposed, scope);
|
|
159
174
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"document-chat-model.js","names":[],"sources":["../../../src/features/ai/document-chat-model.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { EditorBackend } from '../../core/backend'\nimport { documentIdOf } from '../../core/block-transfer'\nimport type { ChangeSet, ChangeSetFeed } from '../../core/change-set'\nimport { aiContextOf, CORE_TARGET_IDS, convertTargetId, conversionsOf, parseConversionTarget, TYPED_BLOCK } from '../../core'\nimport { type Notifier, silentNotifier } from '../../core/notify'\nimport { createStore, type ReadonlyStore } from '../../core/store'\nimport type { Translate } from '../../i18n'\nimport type { ModelRole } from '../../settings'\nimport { type AgentOpReviewState, createAgentOpReview, NO_AGENT_OPS } from './agent-op-review'\nimport type { AgentScope } from './agent-ops'\nimport type { AiTarget } from './ai-apply'\nimport { postAiSse } from './ai-sse'\nimport { documentChatText } from './document-chat'\nimport { type AgentOp, type AgentOpProblem, CHAT_LIMITS, type AiTask, type ChatToolCall, type DocumentChatMessage, type ModelRef } from './protocol'\nimport { notifyAiRateLimited } from './rate-limit'\n\n/**\n * A block generation run from the AI sidebar's quick actions, shown as an assistant turn. Client only: it never goes\n * to the `chat` stream. `markdown` is the streamed result without the task heading (shown with `withTaskHeading`, applied\n * as `taskResultMarkdown`), `target` the apply target captured when it started; only a result that streamed to the end\n * (`done`) can be applied.\n */\nexport interface DocumentChatGeneration {\n task: AiTask\n /** The instruction of a task with `needsPrompt` (empty otherwise). */\n prompt: string\n markdown: string\n target: AiTarget\n done: boolean\n}\n\n/**\n * A chat turn as shown: an assistant turn also keeps the problems of the operations the server dropped, or is a block\n * generation of the sidebar (`generation`).\n */\nexport type DocumentChatTurn =\n | Extract<DocumentChatMessage, { role: 'user' }>\n | (Extract<DocumentChatMessage, { role: 'assistant' }> & {\n problems?: AgentOpProblem[]\n /** The operations aimed at other Documents: they are in the change set panel, not in `ops` (the in-document review), and replay to the model with `ops`. */\n changeSetOps?: AgentOp[]\n generation?: DocumentChatGeneration\n })\n\n/** Snapshot of a conversation; every change replaces `messages`/`ops` (never mutated in place). */\nexport interface DocumentChatState {\n messages: readonly DocumentChatTurn[]\n /** Operation states by operation id. */\n ops: AgentOpReviewState['ops']\n streaming: boolean\n error: string | null\n}\n\nexport interface DocumentChatOptions {\n /** `EditorContext.backend.ai`; without it every question ends in the error state (501), like a server without AI. */\n ai: EditorBackend['ai']\n modelRef: (slot: ModelRole) => ModelRef\n /** Receives the 429 notice. */\n notify: Notifier\n /** The app's translator (`EditorContext.t`): error messages, the 429 notice and the snippets of proposed operations. */\n t: Translate\n /**\n * The change set panel's model (`createChangeSetModel`): receives the change set an answer proposed for other Documents, and\n * hands back the user's decisions on its hunks for the next message. Without it the model's operations aimed at other\n * Documents are still stored by the server, but nobody here shows or reports them.\n */\n changeSets?: ChangeSetFeed\n}\n\nexport interface DocumentChat {\n state: ReadonlyStore<DocumentChatState>\n /**\n * Sends `message` about the editor's current document; `scope` limits proposed edits to the selected blocks (null:\n * whole document). Returns false when it was not answered (error, rate limit, cancelled before any answer): it is\n * then removed again so the caller can restore the input.\n */\n ask: (message: string, editor: Editor, scope: AgentScope | null) => Promise<boolean>\n abort: () => void\n reset: () => void\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 /** Appends the assistant turn of a block generation. `generation` keeps its identity in `state`. */\n addGeneration: (generation: DocumentChatGeneration) => void\n /** Updates `generation` in place (same object) and publishes a new `messages` array. */\n updateGeneration: (generation: DocumentChatGeneration, patch: Partial<Pick<DocumentChatGeneration, 'markdown' | 'done'>>) => void\n /** Removes the turn of `generation`. */\n removeGeneration: (generation: DocumentChatGeneration) => void\n clearError: () => void\n}\n\nconst EMPTY: DocumentChatState = { messages: [], ops: NO_AGENT_OPS, streaming: false, error: null }\n\n/**\n * Document chat state: the conversation lives only here (client side) and goes to the backend's `chat` stream in full with every\n * message, together with the document as it is at that moment and the edit scope. Proposed edit operations open an\n * in-document review; their decisions go to the model with the next message.\n */\nexport function createDocumentChat({ ai, modelRef, notify, t, changeSets }: DocumentChatOptions): DocumentChat {\n const store = createStore<DocumentChatState>(EMPTY)\n const review = createAgentOpReview({ notify: silentNotifier, t })\n let controller: AbortController | null = null\n\n const patch = (changes: Partial<DocumentChatState>) => store.set({ ...store.get(), ...changes })\n\n function abort() {\n controller?.abort()\n }\n\n function reset() {\n abort()\n controller = null\n store.set(EMPTY)\n review.reset()\n changeSets?.reset()\n }\n\n /** Replaces the turn `turn` (by identity) with `next`; returns `next`. */\n function replaceTurn<T extends DocumentChatTurn>(turn: DocumentChatTurn, next: T): T {\n const { messages } = store.get()\n const index = messages.indexOf(turn)\n if (index !== -1) patch({ messages: messages.map((m, i) => (i === index ? next : m)) })\n return next\n }\n\n // The proposed operations live in the review (the chat shows their outcomes inline, so it needs no extra notices); its state is the chat's `ops`.\n review.state.subscribe(({ ops }) => {\n if (ops !== store.get().ops) patch({ ops })\n })\n\n async function ask(message: string, editor: Editor, scope: AgentScope | null): Promise<boolean> {\n if (store.get().streaming || !message.trim()) return false\n if (scope && !scope.ids.length) {\n patch({ error: t('ai.chat.emptyScope') })\n return false\n }\n // The block model of the system prompt: the editor's `aiContext` Block types and host blocks, and what `convert` may turn blocks into.\n // Read before any state changes: a merge error in the contributions throws here and must not leave the chat streaming.\n const { blockTypes, blocks, structure } = aiContextOf(editor)\n const providers = conversionsOf(editor)\n const targets = [\n ...CORE_TARGET_IDS,\n ...blockTypes.map(blockType => convertTargetId({ type: TYPED_BLOCK, blockType: blockType.id })),\n ...blocks.filter(block => parseConversionTarget(providers, block.id) !== null).map(block => block.id),\n ]\n const own = new AbortController()\n controller = own\n const feedback = [...review.unreported(), ...(changeSets?.unreported() ?? [])]\n const userTurn: DocumentChatTurn = { role: 'user', content: message.trim(), ...(feedback.length ? { feedback } : {}) }\n let answer: Extract<DocumentChatTurn, { role: 'assistant' }> = { role: 'assistant', content: '' }\n // The newest chat turns (block generations are the sidebar's own); the request must start with a user message.\n const sent: DocumentChatMessage[] = [...store.get().messages, userTurn].filter(turn => turn.role === 'user' || !turn.generation).slice(-CHAT_LIMITS.messages).map(turn => turn.role === 'user'\n ? { role: 'user', content: turn.content.slice(0, CHAT_LIMITS.message), ...(turn.feedback && { feedback: turn.feedback }) }\n : { role: 'assistant', content: turn.content.slice(0, CHAT_LIMITS.message), ...(turn.ops?.length || turn.changeSetOps?.length ? { ops: [...(turn.ops ?? []), ...(turn.changeSetOps ?? [])] } : {}), ...(turn.dropped ? { dropped: turn.dropped } : {}), ...(turn.tools?.length ? { tools: turn.tools.slice(-CHAT_LIMITS.tools) } : {}) })\n if (sent[0]?.role === 'assistant') sent.shift()\n patch({ error: null, streaming: true, messages: [...store.get().messages, userTurn, answer] })\n const received: { proposal?: { ops: AgentOp[], dropped: AgentOpProblem[] }, changeSet?: ChangeSet } = {}\n const openDocumentId = documentIdOf(editor)\n const body = { messages: sent, document: documentChatText(editor), ...(openDocumentId && { documentId: openDocumentId }), scope: scope?.ids ?? null, nextOp: review.nextOp(), model: modelRef('generate'), blockTypes, blocks, targets, structure }\n const outcome = await postAiSse(ai, 'chat', body, {\n signal: own.signal,\n t,\n onDelta: (text) => {\n if (controller === own) answer = replaceTurn(answer, { ...answer, content: answer.content + text })\n },\n onEvent: (event, data) => {\n if (controller !== own) return\n if (event === 'ops') received.proposal = data as typeof received.proposal\n // Operations aimed at other Documents: stored by the server as a change set.\n else if (event === 'change-set') received.changeSet = data as ChangeSet\n // A read-only tool call the server ran: kept on the turn, so the next message replays it to the model.\n else if (event === 'tool') answer = replaceTurn(answer, { ...answer, tools: [...(answer.tools ?? []), data as ChatToolCall] })\n },\n })\n if (controller !== own) return false // superseded by reset()\n controller = null\n patch({ streaming: false })\n const { ops: proposed = [], dropped = [] } = outcome.status === 'done' ? received.proposal ?? {} : {}\n if (outcome.status === 'done' || (outcome.status === 'aborted' && answer.content)) {\n const reported = feedback.filter(({ outcome: sentOutcome }) => sentOutcome !== 'pending').map(({ op }) => op)\n review.markReported(reported)\n changeSets?.markReported(reported)\n const changeSetOps = received.changeSet?.hunks.map(({ op }) => op) ?? []\n if (received.changeSet) changeSets?.receive(received.changeSet)\n if (proposed.length || dropped.length || changeSetOps.length) {\n answer = replaceTurn(answer, { ...answer, ops: proposed, dropped: dropped.length, problems: dropped, ...(changeSetOps.length && { changeSetOps }) })\n // One numbering for both kinds of operations, so decisions on either are told apart by `op<n>`.\n review.advanceOp(proposed.length + changeSetOps.length)\n if (proposed.length || dropped.length) {\n if (!editor.isDestroyed) review.propose(editor, proposed, scope)\n }\n }\n return true\n }\n // Drop the unanswered turn: a later message should not carry a dangling one in its history.\n patch({ messages: store.get().messages.slice(0, -2) })\n if (outcome.status === 'rate-limited') notifyAiRateLimited(notify, t)\n else if (outcome.status === 'error') patch({ error: outcome.message })\n return false\n }\n\n const decide = review.decide\n const reveal = review.reveal\n\n function addGeneration(generation: DocumentChatGeneration) {\n patch({ error: null, messages: [...store.get().messages, { role: 'assistant', content: '', generation }] })\n }\n\n function updateGeneration(generation: DocumentChatGeneration, changes: Partial<Pick<DocumentChatGeneration, 'markdown' | 'done'>>) {\n Object.assign(generation, changes)\n const { messages } = store.get()\n patch({ messages: messages.map(turn => turn.role === 'assistant' && turn.generation === generation ? { ...turn } : turn) })\n }\n\n function removeGeneration(generation: DocumentChatGeneration) {\n const { messages } = store.get()\n const index = messages.findIndex(turn => turn.role === 'assistant' && turn.generation === generation)\n if (index !== -1) patch({ messages: messages.filter((_, i) => i !== index) })\n }\n\n function clearError() {\n if (store.get().error !== null) patch({ error: null })\n }\n\n return { state: store, ask, abort, reset, decide, reveal, addGeneration, updateGeneration, removeGeneration, clearError }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"document-chat-model.js","names":[],"sources":["../../../src/features/ai/document-chat-model.ts"],"sourcesContent":["import type { Editor } from '@tiptap/core'\nimport type { EditorBackend } from '../../core/backend'\nimport { documentIdOf } from '../../core/block-transfer'\nimport type { ChangeSet, ChangeSetFeed } from '../../core/change-set'\nimport { aiContextOf, CORE_TARGET_IDS, convertTargetId, conversionsOf, parseConversionTarget, TYPED_BLOCK } from '../../core'\nimport { type Notifier, silentNotifier } from '../../core/notify'\nimport { createStore, type ReadonlyStore } from '../../core/store'\nimport type { Translate } from '../../i18n'\nimport type { ModelRole } from '../../settings'\nimport { type AgentOpReview, type AgentOpReviewState, createAgentOpReview, NO_AGENT_OPS } from './agent-op-review'\nimport type { AgentScope } from './agent-ops'\nimport type { AiTarget } from './ai-apply'\nimport { postAiSse } from './ai-sse'\nimport { documentChatText } from './document-chat'\nimport { type AgentOp, type AgentOpProblem, CHAT_LIMITS, type AiTask, type ChatToolCall, type DocumentChatMessage, type ModelRef } from './protocol'\nimport { notifyAiRateLimited } from './rate-limit'\n\n/**\n * A block generation run from the AI sidebar's quick actions, shown as an assistant turn. Client only: it never goes\n * to the `chat` stream. `markdown` is the streamed result without the task heading (shown with `withTaskHeading`, applied\n * as `taskResultMarkdown`), `target` the apply target captured when it started; only a result that streamed to the end\n * (`done`) can be applied.\n */\nexport interface DocumentChatGeneration {\n task: AiTask\n /** The instruction of a task with `needsPrompt` (empty otherwise). */\n prompt: string\n markdown: string\n target: AiTarget\n done: boolean\n}\n\n/**\n * A chat turn as shown: an assistant turn also keeps the problems of the operations the server dropped, or is a block\n * generation of the sidebar (`generation`).\n */\nexport type DocumentChatTurn =\n | Extract<DocumentChatMessage, { role: 'user' }>\n | (Extract<DocumentChatMessage, { role: 'assistant' }> & {\n problems?: AgentOpProblem[]\n /** The operations aimed at other Documents: they are in the change set panel, not in `ops` (the in-document review), and replay to the model with `ops`. */\n changeSetOps?: AgentOp[]\n generation?: DocumentChatGeneration\n })\n\n/** Snapshot of a conversation; every change replaces `messages`/`ops` (never mutated in place). */\nexport interface DocumentChatState {\n messages: readonly DocumentChatTurn[]\n /** Operation states by operation id. */\n ops: AgentOpReviewState['ops']\n streaming: boolean\n error: string | null\n}\n\nexport interface DocumentChatOptions {\n /** `EditorContext.backend.ai`; without it every question ends in the error state (501), like a server without AI. */\n ai: EditorBackend['ai']\n modelRef: (slot: ModelRole) => ModelRef\n /** Receives the 429 notice. */\n notify: Notifier\n /** The app's translator (`EditorContext.t`): error messages, the 429 notice and the snippets of proposed operations. */\n t: Translate\n /**\n * The change set panel's model (`createChangeSetModel`): receives the change set an answer proposed for other Documents, and\n * hands back the user's decisions on its hunks for the next message. Without it the model's operations aimed at other\n * Documents are still stored by the server, but nobody here shows or reports them.\n */\n changeSets?: ChangeSetFeed\n /**\n * The review that shows the proposed operations; default: the chat's own. Pass the one `documentActions` proposes to when a\n * host chat and this one work on the same document: operation IDs (`op<n>`) then come from one counter, an answer's\n * operations are renumbered when they arrive (another proposer may have taken the IDs the request was sent with), and\n * `reset()` forgets the operations of both.\n */\n review?: AgentOpReview\n}\n\nexport interface DocumentChat {\n state: ReadonlyStore<DocumentChatState>\n /**\n * Sends `message` about the editor's current document; `scope` limits proposed edits to the selected blocks (null:\n * whole document). Returns false when it was not answered (error, rate limit, cancelled before any answer): it is\n * then removed again so the caller can restore the input.\n */\n ask: (message: string, editor: Editor, scope: AgentScope | null) => Promise<boolean>\n abort: () => void\n reset: () => void\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 /** Appends the assistant turn of a block generation. `generation` keeps its identity in `state`. */\n addGeneration: (generation: DocumentChatGeneration) => void\n /** Updates `generation` in place (same object) and publishes a new `messages` array. */\n updateGeneration: (generation: DocumentChatGeneration, patch: Partial<Pick<DocumentChatGeneration, 'markdown' | 'done'>>) => void\n /** Removes the turn of `generation`. */\n removeGeneration: (generation: DocumentChatGeneration) => void\n clearError: () => void\n}\n\nconst EMPTY: DocumentChatState = { messages: [], ops: NO_AGENT_OPS, streaming: false, error: null }\n\n/**\n * Document chat state: the conversation lives only here (client side) and goes to the backend's `chat` stream in full with every\n * message, together with the document as it is at that moment and the edit scope. Proposed edit operations open an\n * in-document review; their decisions go to the model with the next message.\n */\nexport function createDocumentChat({ ai, modelRef, notify, t, changeSets, review = createAgentOpReview({ notify: silentNotifier, t }) }: DocumentChatOptions): DocumentChat {\n const store = createStore<DocumentChatState>(EMPTY)\n let controller: AbortController | null = null\n\n const patch = (changes: Partial<DocumentChatState>) => store.set({ ...store.get(), ...changes })\n\n function abort() {\n controller?.abort()\n }\n\n function reset() {\n abort()\n controller = null\n store.set(EMPTY)\n review.reset()\n changeSets?.reset()\n }\n\n /** Replaces the turn `turn` (by identity) with `next`; returns `next`. */\n function replaceTurn<T extends DocumentChatTurn>(turn: DocumentChatTurn, next: T): T {\n const { messages } = store.get()\n const index = messages.indexOf(turn)\n if (index !== -1) patch({ messages: messages.map((m, i) => (i === index ? next : m)) })\n return next\n }\n\n // The proposed operations live in the review (the chat shows their outcomes inline, so it needs no extra notices); its state is the chat's `ops`.\n review.state.subscribe(({ ops }) => {\n if (ops !== store.get().ops) patch({ ops })\n })\n\n async function ask(message: string, editor: Editor, scope: AgentScope | null): Promise<boolean> {\n if (store.get().streaming || !message.trim()) return false\n if (scope && !scope.ids.length) {\n patch({ error: t('ai.chat.emptyScope') })\n return false\n }\n // The block model of the system prompt: the editor's `aiContext` Block types and host blocks, and what `convert` may turn blocks into.\n // Read before any state changes: a merge error in the contributions throws here and must not leave the chat streaming.\n const { blockTypes, blocks, structure } = aiContextOf(editor)\n const providers = conversionsOf(editor)\n const targets = [\n ...CORE_TARGET_IDS,\n ...blockTypes.map(blockType => convertTargetId({ type: TYPED_BLOCK, blockType: blockType.id })),\n ...blocks.filter(block => parseConversionTarget(providers, block.id) !== null).map(block => block.id),\n ]\n const own = new AbortController()\n controller = own\n const feedback = [...review.unreported(), ...(changeSets?.unreported() ?? [])]\n const userTurn: DocumentChatTurn = { role: 'user', content: message.trim(), ...(feedback.length ? { feedback } : {}) }\n let answer: Extract<DocumentChatTurn, { role: 'assistant' }> = { role: 'assistant', content: '' }\n // The newest chat turns (block generations are the sidebar's own); the request must start with a user message.\n const sent: DocumentChatMessage[] = [...store.get().messages, userTurn].filter(turn => turn.role === 'user' || !turn.generation).slice(-CHAT_LIMITS.messages).map(turn => turn.role === 'user'\n ? { role: 'user', content: turn.content.slice(0, CHAT_LIMITS.message), ...(turn.feedback && { feedback: turn.feedback }) }\n : { role: 'assistant', content: turn.content.slice(0, CHAT_LIMITS.message), ...(turn.ops?.length || turn.changeSetOps?.length ? { ops: [...(turn.ops ?? []), ...(turn.changeSetOps ?? [])] } : {}), ...(turn.dropped ? { dropped: turn.dropped } : {}), ...(turn.tools?.length ? { tools: turn.tools.slice(-CHAT_LIMITS.tools) } : {}) })\n if (sent[0]?.role === 'assistant') sent.shift()\n patch({ error: null, streaming: true, messages: [...store.get().messages, userTurn, answer] })\n const received: { proposal?: { ops: AgentOp[], dropped: AgentOpProblem[] }, changeSet?: ChangeSet } = {}\n const openDocumentId = documentIdOf(editor)\n const sentNextOp = review.nextOp()\n // The server numbers the operations from `sentNextOp`; another source proposing to the same review meanwhile (a host chat's `doc.propose_ops`) may have taken those IDs by the time they arrive, so they are renumbered then.\n const numbered = (proposal: NonNullable<typeof received.proposal>, changeSetOps: number): NonNullable<typeof received.proposal> => {\n const first = review.nextOp()\n if (first === sentNextOp) {\n review.advanceOp(proposal.ops.length + changeSetOps)\n return proposal\n }\n // Past the IDs the server gave (the change set's operations keep theirs: its store decides by them).\n const start = Math.max(first, sentNextOp + proposal.ops.length + changeSetOps)\n review.advanceOp(start + proposal.ops.length - first)\n return { ...proposal, ops: proposal.ops.map((op, index) => ({ ...op, op: `op${start + index}` })) }\n }\n const body = { messages: sent, document: documentChatText(editor), ...(openDocumentId && { documentId: openDocumentId }), scope: scope?.ids ?? null, nextOp: sentNextOp, model: modelRef('generate'), blockTypes, blocks, targets, structure }\n const outcome = await postAiSse(ai, 'chat', body, {\n signal: own.signal,\n t,\n onDelta: (text) => {\n if (controller === own) answer = replaceTurn(answer, { ...answer, content: answer.content + text })\n },\n onEvent: (event, data) => {\n if (controller !== own) return\n if (event === 'ops') received.proposal = numbered(data as NonNullable<typeof received.proposal>, received.changeSet?.hunks.length ?? 0)\n // Operations aimed at other Documents: stored by the server as a change set.\n else if (event === 'change-set') received.changeSet = data as ChangeSet\n // A read-only tool call the server ran: kept on the turn, so the next message replays it to the model.\n else if (event === 'tool') answer = replaceTurn(answer, { ...answer, tools: [...(answer.tools ?? []), data as ChatToolCall] })\n },\n })\n if (controller !== own) return false // superseded by reset()\n controller = null\n patch({ streaming: false })\n const { ops: proposed = [], dropped = [] } = outcome.status === 'done' ? received.proposal ?? {} : {}\n if (outcome.status === 'done' || (outcome.status === 'aborted' && answer.content)) {\n const reported = feedback.filter(({ outcome: sentOutcome }) => sentOutcome !== 'pending').map(({ op }) => op)\n review.markReported(reported)\n changeSets?.markReported(reported)\n const changeSetOps = received.changeSet?.hunks.map(({ op }) => op) ?? []\n if (received.changeSet) changeSets?.receive(received.changeSet)\n if (proposed.length || dropped.length || changeSetOps.length) {\n answer = replaceTurn(answer, { ...answer, ops: proposed, dropped: dropped.length, problems: dropped, ...(changeSetOps.length && { changeSetOps }) })\n // The operations got their IDs when the `ops` event arrived (`numbered`), so decisions on either kind are told apart by `op<n>`.\n if (proposed.length || dropped.length) {\n if (!editor.isDestroyed) review.propose(editor, proposed, scope)\n }\n }\n return true\n }\n // Drop the unanswered turn: a later message should not carry a dangling one in its history.\n patch({ messages: store.get().messages.slice(0, -2) })\n if (outcome.status === 'rate-limited') notifyAiRateLimited(notify, t)\n else if (outcome.status === 'error') patch({ error: outcome.message })\n return false\n }\n\n const decide = review.decide\n const reveal = review.reveal\n\n function addGeneration(generation: DocumentChatGeneration) {\n patch({ error: null, messages: [...store.get().messages, { role: 'assistant', content: '', generation }] })\n }\n\n function updateGeneration(generation: DocumentChatGeneration, changes: Partial<Pick<DocumentChatGeneration, 'markdown' | 'done'>>) {\n Object.assign(generation, changes)\n const { messages } = store.get()\n patch({ messages: messages.map(turn => turn.role === 'assistant' && turn.generation === generation ? { ...turn } : turn) })\n }\n\n function removeGeneration(generation: DocumentChatGeneration) {\n const { messages } = store.get()\n const index = messages.findIndex(turn => turn.role === 'assistant' && turn.generation === generation)\n if (index !== -1) patch({ messages: messages.filter((_, i) => i !== index) })\n }\n\n function clearError() {\n if (store.get().error !== null) patch({ error: null })\n }\n\n return { state: store, ask, abort, reset, decide, reveal, addGeneration, updateGeneration, removeGeneration, clearError }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parseFrontmatter } from "../../core/frontmatter.js";
|
|
2
2
|
import { MAX_MEDIA_BYTES, MEDIA_TYPES, acceptFor, mediaKindOf, mediaMime } from "./schema.js";
|
|
3
|
+
import { createAudioRecorder } from "./audio-recorder.js";
|
|
3
4
|
import { AUDIO_RECORDER_OPEN, Media, fileNodeFor, formatBytes, mediaNodeFor, openAudioRecorder, pickFiles, uploadOf } from "./upload.js";
|
|
4
5
|
import { MAX_TEXT_BYTES, fileImport, fileImportOptions, isTextFile } from "./file-import.js";
|
|
5
6
|
import { parseBibtex } from "./importers/bibtex.js";
|
|
@@ -9,5 +10,4 @@ import { cueTime, parseSubtitles, subtitleImporter } from "./importers/subtitle.
|
|
|
9
10
|
import { FILE_IMPORTERS } from "./importers/index.js";
|
|
10
11
|
import { mediaBlocks, playerOptions } from "./node-views.js";
|
|
11
12
|
import { mediaFeatureCore } from "./feature.js";
|
|
12
|
-
import { createAudioRecorder } from "./audio-recorder.js";
|
|
13
13
|
export { AUDIO_RECORDER_OPEN, FILE_IMPORTERS, MAX_MEDIA_BYTES, MAX_TEXT_BYTES, MEDIA_TYPES, Media, acceptFor, bibImporter, createAudioRecorder, cueTime, fileImport, fileImportOptions, fileNodeFor, formatBytes, isTextFile, markdownImporter, mediaBlocks, mediaFeatureCore, mediaKindOf, mediaMime, mediaNodeFor, openAudioRecorder, parseBibtex, parseFrontmatter, parseSubtitles, pickFiles, playerOptions, subtitleImporter, uploadOf };
|
|
@@ -5,8 +5,8 @@ import { Icon } from "../../../react/Icon.js";
|
|
|
5
5
|
import { useEditorI18n } from "../../../react/use-editor-i18n.js";
|
|
6
6
|
import { Button } from "../../../react/ui/button.js";
|
|
7
7
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "../../../react/ui/dialog.js";
|
|
8
|
-
import { useNotifier } from "../../../react/use-notifier.js";
|
|
9
8
|
import "../upload.js";
|
|
9
|
+
import { useNotifier } from "../../../react/use-notifier.js";
|
|
10
10
|
import { useAudioRecorder } from "./use-audio-recorder.js";
|
|
11
11
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
12
12
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useEditorContext } from "../../react/context.js";
|
|
2
2
|
import { useEditorI18n } from "../../react/use-editor-i18n.js";
|
|
3
|
-
import { useNotifier } from "../../react/use-notifier.js";
|
|
4
3
|
import { fileImport, fileImportOptions } from "./file-import.js";
|
|
5
4
|
import { playerOptions } from "./node-views.js";
|
|
6
5
|
import { mediaFeatureCore } from "./feature.js";
|
|
6
|
+
import { useNotifier } from "../../react/use-notifier.js";
|
|
7
7
|
import { useAudioRecorder } from "./react/use-audio-recorder.js";
|
|
8
8
|
import { AudioRecorderModal } from "./react/AudioRecorderModal.js";
|
|
9
9
|
import { AudioView } from "./react/AudioView.js";
|
|
@@ -3,8 +3,8 @@ import { AI_LIMITS } from "../../features/ai/protocol/tasks.js";
|
|
|
3
3
|
import { TYPED_BLOCK } from "../../core/typed-block.js";
|
|
4
4
|
import { newBlockId } from "../../core/schema/block-ids.js";
|
|
5
5
|
import { aiContextOf } from "../../core/pipeline.js";
|
|
6
|
-
import { modelProfiles } from "../../features/ai/model-profiles.js";
|
|
7
6
|
import { transferKindOf, transferSourceHash } from "../../features/transfer/transforms.js";
|
|
7
|
+
import { modelProfiles } from "../../features/ai/model-profiles.js";
|
|
8
8
|
import { streamAiText } from "./ai-edit-session.js";
|
|
9
9
|
import { emptyDerivedBlock, runDerivedSession } from "./derive.js";
|
|
10
10
|
//#region src/integrations/composite-ai/transfer-transforms.ts
|
|
@@ -41,20 +41,20 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
41
41
|
settingsOpen: import("vue").WritableComputedRef<boolean, boolean>;
|
|
42
42
|
settingsTab: import("vue").WritableComputedRef<string | undefined, string | undefined>;
|
|
43
43
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
44
|
+
"update:modelValue": (markdown: string) => any;
|
|
45
|
+
ready: (editor: Editor) => any;
|
|
44
46
|
"update:sidebarOpen": (value: boolean) => any;
|
|
45
47
|
"update:commentsOpen": (value: boolean) => any;
|
|
46
|
-
ready: (editor: Editor) => any;
|
|
47
|
-
"update:modelValue": (markdown: string) => any;
|
|
48
48
|
metadataCreate: (list: string, item: MetadataItem) => any;
|
|
49
49
|
contentError: (error: Error) => any;
|
|
50
50
|
settingsOpen: (tab?: string | undefined) => any;
|
|
51
51
|
frontmatter: (frontmatter: string | null) => any;
|
|
52
52
|
diagnostics: (diagnostics: MarkdownDiagnostic[]) => any;
|
|
53
53
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
54
|
+
"onUpdate:modelValue"?: ((markdown: string) => any) | undefined;
|
|
55
|
+
onReady?: ((editor: Editor) => any) | undefined;
|
|
54
56
|
"onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
|
|
55
57
|
"onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
|
|
56
|
-
onReady?: ((editor: Editor) => any) | undefined;
|
|
57
|
-
"onUpdate:modelValue"?: ((markdown: string) => any) | undefined;
|
|
58
58
|
onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
|
|
59
59
|
onContentError?: ((error: Error) => any) | undefined;
|
|
60
60
|
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
package/dist/presets/compose.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { defineEditor } from "../core/define-editor.js";
|
|
2
2
|
import { coreContentFeature, coreUiFeature } from "../core/core-feature.js";
|
|
3
3
|
import { CommentMark } from "../features/comments/schema.js";
|
|
4
|
-
import { copyTransform } from "../features/transfer/transforms.js";
|
|
5
4
|
import { mentionsFeature } from "../features/mentions/index.js";
|
|
5
|
+
import { copyTransform } from "../features/transfer/transforms.js";
|
|
6
6
|
import { tablesFeature } from "../features/tables/feature.js";
|
|
7
7
|
import { compositeMediaFeature } from "../integrations/composite-media/index.js";
|
|
8
8
|
import { CATALOG, catalogIds } from "./catalog.js";
|
package/dist/presets/layer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
+
import { composeBundle, composeEditor } from "./compose.js";
|
|
1
2
|
import { bulletsTransform, summaryTransform } from "../integrations/composite-ai/transfer-transforms.js";
|
|
2
3
|
import { compositeAiFeature } from "../integrations/composite-ai/index.js";
|
|
3
4
|
import { transcriptAiFeature } from "../integrations/transcript-ai/index.js";
|
|
4
|
-
import { composeBundle, composeEditor } from "./compose.js";
|
|
5
5
|
//#region src/presets/layer.ts
|
|
6
6
|
/**
|
|
7
7
|
* What the UI layers share: the bindings that are no component and the same for every framework, the AI integrations and
|
package/dist/tree/tree-host.js
CHANGED
|
@@ -2,8 +2,8 @@ import { isTypedBlock, newTypedBlockJSON } from "../core/typed-block.js";
|
|
|
2
2
|
import { findBlockById } from "../core/schema/block-ids.js";
|
|
3
3
|
import { blockTypesOf, compositeBasketsOf } from "../features/composites/nodes.js";
|
|
4
4
|
import { revealBlockInEditor } from "../outline/heading-outline.js";
|
|
5
|
-
import { compositeFocusOf } from "../features/composites/focus.js";
|
|
6
5
|
import { pickFiles } from "../features/media/upload.js";
|
|
6
|
+
import { compositeFocusOf } from "../features/composites/focus.js";
|
|
7
7
|
import { moveBlockInto, moveIntoVerdict } from "../features/composites/outline.js";
|
|
8
8
|
import { readOutline } from "./tree-items.js";
|
|
9
9
|
//#region src/tree/tree-host.ts
|
|
@@ -82,18 +82,18 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
82
82
|
editor: import("vue").ComputedRef<Editor | undefined>;
|
|
83
83
|
preset: ComposedEditor<Component>;
|
|
84
84
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
85
|
+
"update:modelValue": (content: JSONContent) => any;
|
|
86
|
+
ready: (editor: Editor) => any;
|
|
85
87
|
"update:sidebarOpen": (value: boolean) => any;
|
|
86
88
|
"update:commentsOpen": (value: boolean) => any;
|
|
87
|
-
ready: (editor: Editor) => any;
|
|
88
|
-
"update:modelValue": (content: JSONContent) => any;
|
|
89
89
|
metadataCreate: (list: string, item: MetadataItem) => any;
|
|
90
90
|
contentError: (error: Error) => any;
|
|
91
91
|
settingsOpen: (tab?: string | undefined) => any;
|
|
92
92
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
93
|
+
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
94
|
+
onReady?: ((editor: Editor) => any) | undefined;
|
|
93
95
|
"onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
|
|
94
96
|
"onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
|
|
95
|
-
onReady?: ((editor: Editor) => any) | undefined;
|
|
96
|
-
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
97
97
|
onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
|
|
98
98
|
onContentError?: ((error: Error) => any) | undefined;
|
|
99
99
|
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
|
@@ -41,18 +41,18 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
41
41
|
editor: import("vue").ComputedRef<Editor | undefined>;
|
|
42
42
|
preset: EditorPreset<Component>;
|
|
43
43
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
44
|
+
"update:modelValue": (content: JSONContent) => any;
|
|
45
|
+
ready: (editor: Editor) => any;
|
|
44
46
|
"update:sidebarOpen": (value: boolean) => any;
|
|
45
47
|
"update:commentsOpen": (value: boolean) => any;
|
|
46
|
-
ready: (editor: Editor) => any;
|
|
47
|
-
"update:modelValue": (content: JSONContent) => any;
|
|
48
48
|
metadataCreate: (list: string, item: MetadataItem) => any;
|
|
49
49
|
contentError: (error: Error) => any;
|
|
50
50
|
settingsOpen: (tab?: string | undefined) => any;
|
|
51
51
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
52
|
+
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
53
|
+
onReady?: ((editor: Editor) => any) | undefined;
|
|
52
54
|
"onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
|
|
53
55
|
"onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
|
|
54
|
-
onReady?: ((editor: Editor) => any) | undefined;
|
|
55
|
-
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
56
56
|
onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
|
|
57
57
|
onContentError?: ((error: Error) => any) | undefined;
|
|
58
58
|
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
|
@@ -43,18 +43,18 @@ declare const __VLS_export: import("vue").DefineComponent<__VLS_PublicProps, {
|
|
|
43
43
|
settingsOpen: import("vue").Ref<boolean, boolean>;
|
|
44
44
|
settingsTab: import("vue").Ref<string | undefined, string | undefined>;
|
|
45
45
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
|
|
46
|
+
"update:modelValue": (content: JSONContent) => any;
|
|
47
|
+
ready: (editor: Editor) => any;
|
|
46
48
|
"update:sidebarOpen": (value: boolean) => any;
|
|
47
49
|
"update:commentsOpen": (value: boolean) => any;
|
|
48
|
-
ready: (editor: Editor) => any;
|
|
49
|
-
"update:modelValue": (content: JSONContent) => any;
|
|
50
50
|
metadataCreate: (list: string, item: MetadataItem) => any;
|
|
51
51
|
contentError: (error: Error) => any;
|
|
52
52
|
settingsOpen: (tab?: string | undefined) => any;
|
|
53
53
|
}, string, import("vue").PublicProps, Readonly<__VLS_PublicProps> & Readonly<{
|
|
54
|
+
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
55
|
+
onReady?: ((editor: Editor) => any) | undefined;
|
|
54
56
|
"onUpdate:sidebarOpen"?: ((value: boolean) => any) | undefined;
|
|
55
57
|
"onUpdate:commentsOpen"?: ((value: boolean) => any) | undefined;
|
|
56
|
-
onReady?: ((editor: Editor) => any) | undefined;
|
|
57
|
-
"onUpdate:modelValue"?: ((content: JSONContent) => any) | undefined;
|
|
58
58
|
onMetadataCreate?: ((list: string, item: MetadataItem) => any) | undefined;
|
|
59
59
|
onContentError?: ((error: Error) => any) | undefined;
|
|
60
60
|
onSettingsOpen?: ((tab?: string | undefined) => any) | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaile/forge-editor",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vue and React editor library on TipTap v3 (Vue: Nuxt UI; React: shadcn/ui): framework-free features with Vue and React bindings, simple and advanced presets, tree-shakeable entries. Built ESM with type declarations.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -697,7 +697,7 @@
|
|
|
697
697
|
"@iconify/vue": "^5.0.3",
|
|
698
698
|
"@lezer/highlight": "^1.2.1",
|
|
699
699
|
"@nuxt/ui": "^4.11.1",
|
|
700
|
-
"@skaile/forge-app-actions": "^0.1.0",
|
|
700
|
+
"@skaile/forge-app-actions": "^0.1.0 || ^0.2.0",
|
|
701
701
|
"@tiptap/core": "^3.31.3",
|
|
702
702
|
"@tiptap/pm": "^3.31.3",
|
|
703
703
|
"@tiptap/react": "^3.31.3",
|
|
@@ -761,7 +761,7 @@
|
|
|
761
761
|
"@iconify/react": "^6.0.2",
|
|
762
762
|
"@iconify/vue": "^5.0.3",
|
|
763
763
|
"@lezer/highlight": "^1.2.1",
|
|
764
|
-
"@skaile/forge-app-actions": "^0.
|
|
764
|
+
"@skaile/forge-app-actions": "^0.2.0",
|
|
765
765
|
"@tiptap/react": "3.31.3",
|
|
766
766
|
"@types/react": "^19.3.0",
|
|
767
767
|
"@types/react-dom": "^19.3.0",
|