@pixerate/editor 0.234.0 → 0.236.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.
- package/dist/agent/index.cjs +286 -27
- package/dist/agent/index.d.cts +1 -0
- package/dist/agent/index.d.ts +1 -0
- package/dist/agent/index.js +261 -2
- package/dist/agent/reviewTools.d.cts +16 -0
- package/dist/agent/reviewTools.d.ts +16 -0
- package/dist/animation-editor/controller.d.cts +25 -0
- package/dist/animation-editor/controller.d.ts +25 -0
- package/dist/animation-editor/index.cjs +159 -50
- package/dist/animation-editor/index.d.cts +1 -0
- package/dist/animation-editor/index.d.ts +1 -0
- package/dist/animation-editor/index.js +115 -6
- package/dist/animation-editor/layers.d.cts +2 -0
- package/dist/animation-editor/layers.d.ts +2 -0
- package/dist/animation-editor/sequence.d.cts +33 -0
- package/dist/animation-editor/sequence.d.ts +33 -0
- package/dist/{chunk-PI2UYMZY.js → chunk-FSLKBQU7.js} +269 -15
- package/dist/{chunk-I277KJLQ.js → chunk-G3RUKJ5C.js} +105 -1
- package/dist/{chunk-6RZVFYGJ.cjs → chunk-JWCKJNDR.cjs} +105 -1
- package/dist/{chunk-3MO3ZS7E.js → chunk-PHYOZK3B.js} +59 -311
- package/dist/{chunk-CZBRY4SW.cjs → chunk-WPWHT4Z6.cjs} +122 -374
- package/dist/{chunk-O7QXVGMP.cjs → chunk-WV5WTOC6.cjs} +295 -41
- package/dist/effects/index.cjs +19 -11
- package/dist/effects/index.d.cts +1 -0
- package/dist/effects/index.d.ts +1 -0
- package/dist/effects/index.js +9 -1
- package/dist/effects/template-tools.d.cts +25 -0
- package/dist/effects/template-tools.d.ts +25 -0
- package/dist/effects/templates.d.cts +30 -1
- package/dist/effects/templates.d.ts +30 -1
- package/dist/extensions/index.cjs +6 -6
- package/dist/extensions/index.js +10 -10
- package/dist/index.cjs +11 -11
- package/dist/index.js +10 -10
- package/dist/video-editor/controller.d.cts +26 -0
- package/dist/video-editor/controller.d.ts +26 -0
- package/dist/video-editor/index.cjs +187 -42
- package/dist/video-editor/index.d.cts +1 -0
- package/dist/video-editor/index.d.ts +1 -0
- package/dist/video-editor/index.js +145 -0
- package/dist/video-editor/sequence.d.cts +38 -0
- package/dist/video-editor/sequence.d.ts +38 -0
- package/package.json +1 -1
- package/skills/pixerate-rich-text/SKILL.md +28 -1
- package/skills/pixerate-rich-text-revise/SKILL.md +8 -1
|
@@ -153,6 +153,13 @@ import {
|
|
|
153
153
|
import {
|
|
154
154
|
namedWithSuggestion
|
|
155
155
|
} from "../chunk-EUKY4KFX.js";
|
|
156
|
+
import {
|
|
157
|
+
effectLayerRole,
|
|
158
|
+
effectLayerSpan,
|
|
159
|
+
effectSequenceTiming,
|
|
160
|
+
effectTemplateTools,
|
|
161
|
+
fillEffectTemplate
|
|
162
|
+
} from "../chunk-G3RUKJ5C.js";
|
|
156
163
|
import "../chunk-EHLOCVWB.js";
|
|
157
164
|
import {
|
|
158
165
|
CAMERA_MOVE_MAX_MS,
|
|
@@ -3454,6 +3461,106 @@ var VideoPreviewEngine = class {
|
|
|
3454
3461
|
}
|
|
3455
3462
|
};
|
|
3456
3463
|
|
|
3464
|
+
// src/video-editor/sequence.ts
|
|
3465
|
+
var svgUrl = (svg) => svg.trimStart().startsWith("<") ? `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}` : svg;
|
|
3466
|
+
var GROUPS = ["base", "background", "subject", "overlay", "text"];
|
|
3467
|
+
function videoEditsFromSequence(project, sequence, options = {}) {
|
|
3468
|
+
const at = options.at ?? videoProjectDuration(project);
|
|
3469
|
+
const W = project.width;
|
|
3470
|
+
const H = project.height;
|
|
3471
|
+
const sx = W / sequence.stage.width;
|
|
3472
|
+
const sy = H / sequence.stage.height;
|
|
3473
|
+
const timing = effectSequenceTiming(sequence);
|
|
3474
|
+
const provider = options.provider ? { provider: options.provider } : {};
|
|
3475
|
+
const notes = [];
|
|
3476
|
+
const usedClips = new Set(allVideoClipIds(project));
|
|
3477
|
+
const usedTracks = new Set(project.tracks.map((t) => t.id));
|
|
3478
|
+
const usedAssets = new Set(project.assets.map((a) => a.id));
|
|
3479
|
+
const fresh = (used, base) => {
|
|
3480
|
+
for (let i = 1; ; i++) if (!used.has(`${base}${i}`)) return used.add(`${base}${i}`), `${base}${i}`;
|
|
3481
|
+
};
|
|
3482
|
+
const edits = [];
|
|
3483
|
+
const assets = [];
|
|
3484
|
+
const placeholders = {};
|
|
3485
|
+
const items = [];
|
|
3486
|
+
const placement = (r) => {
|
|
3487
|
+
if (!r) return {};
|
|
3488
|
+
const x = (r.x + r.width / 2) * sx - W / 2;
|
|
3489
|
+
const y = (r.y + r.height / 2) * sy - H / 2;
|
|
3490
|
+
const scale = Math.min(r.width * sx / W, r.height * sy / H);
|
|
3491
|
+
return { transform: { x: Math.round(x * 100) / 100, y: Math.round(y * 100) / 100, scale: Math.round(scale * 1e3) / 1e3 } };
|
|
3492
|
+
};
|
|
3493
|
+
const clipOf = (layer, start, duration, shot) => {
|
|
3494
|
+
const id = fresh(usedClips, "tpl-c");
|
|
3495
|
+
const common = { id, start, duration, ...layer.opacity !== void 0 && layer.opacity < 1 ? { opacity: layer.opacity } : {}, ...layer.fade ? { fadeIn: layer.fade, fadeOut: layer.fade } : {}, ...placement(layer.rect) };
|
|
3496
|
+
const effects = layer.effect ? { effects: [{ id: "fx1", effect: layer.effect, ...provider, ...layer.values ? { values: layer.values } : {} }] } : {};
|
|
3497
|
+
const s = layer.subject;
|
|
3498
|
+
if (!s) return { ...common, type: "effect", effect: layer.effect, ...provider, ...layer.values ? { values: layer.values } : {} };
|
|
3499
|
+
if ("slot" in s) {
|
|
3500
|
+
(placeholders[s.slot] ??= []).push(id);
|
|
3501
|
+
if (layer.effect) notes.push(`Shot ${shot + 1}: the "${s.slot}" slot is a grey placeholder; when you swap in the picture, apply ${layer.effect} to it again (apply_effect).`);
|
|
3502
|
+
return { ...common, type: "color", color: "#475569", name: `Slot "${s.slot}"` };
|
|
3503
|
+
}
|
|
3504
|
+
if ("text" in s) return { ...common, type: "text", text: s.text, ...s.font ? { fontFamily: s.font } : {}, ...typeof s.weight === "number" ? { fontWeight: s.weight } : {}, ...effects };
|
|
3505
|
+
const asset = fresh(usedAssets, "tpl-a");
|
|
3506
|
+
assets.push(asset);
|
|
3507
|
+
if ("video" in s) {
|
|
3508
|
+
notes.push(`Shot ${shot + 1}: a video's length is not known here, so its asset is given the clip's length (${Math.round(duration)} ms); correct it with update_video_asset if it is longer.`);
|
|
3509
|
+
edits.push({ op: "add_asset", asset: { id: asset, kind: "video", src: s.video, duration } });
|
|
3510
|
+
} else edits.push({ op: "add_asset", asset: { id: asset, kind: "image", src: "image" in s ? s.image : svgUrl(s.svg) } });
|
|
3511
|
+
return { ...common, type: "media", asset, fit: "contain", ...effects };
|
|
3512
|
+
};
|
|
3513
|
+
sequence.shots.forEach((shot, i) => {
|
|
3514
|
+
const shotStart = timing.starts[i];
|
|
3515
|
+
const shotEnd = timing.starts[i + 1] ?? timing.total;
|
|
3516
|
+
let base;
|
|
3517
|
+
shot.layers.forEach((layer, j) => {
|
|
3518
|
+
const span = effectLayerSpan(layer, i, timing.starts, timing.total);
|
|
3519
|
+
if (span.end - span.start <= 0) return void notes.push(`Shot ${i + 1}, layer ${j + 1} lasts no time, so it was left out.`);
|
|
3520
|
+
const clip = clipOf(layer, at + span.start, span.end - span.start, i);
|
|
3521
|
+
const isBase = j === 0 && !base && span.start === shotStart && span.end === shotEnd && effectLayerRole(layer) === "background" && !layer.rect;
|
|
3522
|
+
const item = { group: isBase ? "base" : effectLayerRole(layer), clip };
|
|
3523
|
+
if (isBase) base = item;
|
|
3524
|
+
items.push(item);
|
|
3525
|
+
});
|
|
3526
|
+
if (!base) {
|
|
3527
|
+
base = { group: "base", clip: { id: fresh(usedClips, "tpl-c"), type: "color", color: "#000000", name: `Shot ${i + 1}`, start: at + shotStart, duration: shotEnd - shotStart } };
|
|
3528
|
+
items.push(base);
|
|
3529
|
+
}
|
|
3530
|
+
if (i > 0 && shot.transition) {
|
|
3531
|
+
base.clip.transition = { type: "effect", duration: shot.transition.ms ?? 800, effect: { effect: shot.transition.effect, ...provider, ...shot.transition.values ? { values: shot.transition.values } : {} } };
|
|
3532
|
+
if (shot.layers.length > 1) notes.push(`Shot ${i + 1}: its transition blends the clips on the base track; the layers above it cut.`);
|
|
3533
|
+
}
|
|
3534
|
+
const words = (text, y, size, name) => items.push({ group: "text", clip: { id: fresh(usedClips, "tpl-c"), type: "text", text, name, fontSize: Math.round(size), start: at + shotStart, duration: shotEnd - shotStart, transform: { x: 0, y: Math.round(y) } } });
|
|
3535
|
+
if (shot.title) words(shot.title, 0, H * 0.09, "Title");
|
|
3536
|
+
if (shot.caption) words(shot.caption, H * 0.35, H * 0.045, "Caption");
|
|
3537
|
+
if (shot.camera?.in !== void 0 || shot.camera?.during !== void 0 || shot.camera?.framing) notes.push(`Shot ${i + 1}: its camera framing and moves were not carried over; set them with set_clip_camera_move on its clips.`);
|
|
3538
|
+
});
|
|
3539
|
+
const lanes = [];
|
|
3540
|
+
for (const group of GROUPS) {
|
|
3541
|
+
const mine = [];
|
|
3542
|
+
for (const item of items.filter((x) => x.group === group).sort((a, b) => a.clip.start - b.clip.start)) {
|
|
3543
|
+
let lane = group === "base" ? mine[0] : mine.find((l) => l.ends.every((e) => e <= item.clip.start + 1e-6));
|
|
3544
|
+
if (!lane) mine.push(lane = { group, id: fresh(usedTracks, "tpl-v"), ends: [], clips: [] });
|
|
3545
|
+
lane.ends.push(item.clip.start + item.clip.duration);
|
|
3546
|
+
lane.clips.push(item);
|
|
3547
|
+
}
|
|
3548
|
+
lanes.push(...mine);
|
|
3549
|
+
}
|
|
3550
|
+
for (const lane of lanes) edits.push({ op: "add_track", track: { id: lane.id, kind: "video", name: `Template ${lane.group}` }, at: 0 });
|
|
3551
|
+
for (const lane of lanes) for (const item of lane.clips) edits.push({ op: "add_clip", track: lane.id, clip: item.clip });
|
|
3552
|
+
return {
|
|
3553
|
+
edits,
|
|
3554
|
+
clips: lanes.flatMap((l) => l.clips.map((c) => c.clip.id)),
|
|
3555
|
+
tracks: lanes.map((l) => l.id).reverse(),
|
|
3556
|
+
assets,
|
|
3557
|
+
start: at,
|
|
3558
|
+
end: at + timing.total,
|
|
3559
|
+
placeholders,
|
|
3560
|
+
notes
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3457
3564
|
// src/video-editor/controller.ts
|
|
3458
3565
|
var VideoEditorController = class _VideoEditorController {
|
|
3459
3566
|
history;
|
|
@@ -3818,6 +3925,29 @@ var VideoEditorController = class _VideoEditorController {
|
|
|
3818
3925
|
if (place) checkVideoEffectPlace(d, place);
|
|
3819
3926
|
return d;
|
|
3820
3927
|
}
|
|
3928
|
+
/**
|
|
3929
|
+
* Fills a provider sequence template and puts it on the timeline at `at` (default the end), on
|
|
3930
|
+
* new tracks above the others (see `videoEditsFromSequence`). Values are checked first and every
|
|
3931
|
+
* problem is refused at once. One undo step. Returns the new tracks, clips and assets, where it
|
|
3932
|
+
* starts and ends, the placeholder clips standing in for open slots, and notes.
|
|
3933
|
+
*/
|
|
3934
|
+
async fillTemplate(template, values = {}, options = {}) {
|
|
3935
|
+
const reason = this.readOnlyReason();
|
|
3936
|
+
if (reason) return this.refused(reason);
|
|
3937
|
+
if (options.at !== void 0 && !(Number.isFinite(options.at) && options.at >= 0)) return this.refused(`"at" is a time in ms, 0 or more (got ${options.at}).`);
|
|
3938
|
+
let filled;
|
|
3939
|
+
try {
|
|
3940
|
+
filled = await fillEffectTemplate(this.effects.provider, template, values, options.name ? { name: options.name } : {});
|
|
3941
|
+
} catch (error) {
|
|
3942
|
+
if (isEffectError(error) || error instanceof Error) return this.refused(error.message);
|
|
3943
|
+
throw error;
|
|
3944
|
+
}
|
|
3945
|
+
const made = videoEditsFromSequence(this.getState().project, filled.sequence, { at: options.at, provider: this.effects.provider.id });
|
|
3946
|
+
const r = this.apply(made.edits, `Template ${filled.template.name}`);
|
|
3947
|
+
if (!r.ok) return r;
|
|
3948
|
+
const { edits: _edits, notes, ...rest } = made;
|
|
3949
|
+
return { ok: true, template, ...rest, openSlots: filled.openSlots, notes: [...filled.notes, ...notes] };
|
|
3950
|
+
}
|
|
3821
3951
|
refused(message) {
|
|
3822
3952
|
return this.refuse(message);
|
|
3823
3953
|
}
|
|
@@ -5923,6 +6053,20 @@ function createVideoEffectsToolProvider(controller, options = {}) {
|
|
|
5923
6053
|
subscribe: (notify) => controller.subscribe(() => notify()),
|
|
5924
6054
|
extraTools: (ctx) => [
|
|
5925
6055
|
...videoCameraMoveTools(controller, ctx),
|
|
6056
|
+
...effectTemplateTools(
|
|
6057
|
+
{
|
|
6058
|
+
provider: () => controller.effectsProvider,
|
|
6059
|
+
fillDetails: `It goes on the timeline at "at" (default the end of the project), on new tracks above the others: each shot's bottom layer on a base track (with the shot's transition), other layers as effect, picture and text clips on tracks by role, titles and captions as text clips; an open subject slot becomes a grey placeholder clip named after it. Camera moves are not carried over. Returns { template, tracks, clips, assets, start, end (ms), placeholders (clip ids by open slot), openSlots, notes }. To fill an open slot, add the picture with add_video_asset and put it on the placeholder's track at its start with add_video_clip in overwrite mode.`,
|
|
6060
|
+
fillOptions: { at: { type: "number", minimum: 0, description: "Where on the timeline it starts, ms (default the end of the project)." } },
|
|
6061
|
+
fill: async (template, values, options2) => {
|
|
6062
|
+
const r = await controller.fillTemplate(template, values, { name: options2.name, at: options2.at });
|
|
6063
|
+
if (!r.ok) throw new Error(r.error);
|
|
6064
|
+
const { ok: _ok, ...rest } = r;
|
|
6065
|
+
return rest;
|
|
6066
|
+
}
|
|
6067
|
+
},
|
|
6068
|
+
ctx
|
|
6069
|
+
),
|
|
5926
6070
|
{
|
|
5927
6071
|
name: "add_video_effect_clip",
|
|
5928
6072
|
title: "Add an effect clip",
|
|
@@ -6684,6 +6828,7 @@ export {
|
|
|
6684
6828
|
videoEaseBezier,
|
|
6685
6829
|
videoEaseProgress,
|
|
6686
6830
|
videoEaseToAnimationEasing,
|
|
6831
|
+
videoEditsFromSequence,
|
|
6687
6832
|
videoEffectFrameTime,
|
|
6688
6833
|
videoEffectLayerKey,
|
|
6689
6834
|
videoEffectValuesAt,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An effects provider's `EffectSequence` (what a sequence template fills to) as video edits, so a
|
|
3
|
+
* template lands on the timeline as one undo step:
|
|
4
|
+
*
|
|
5
|
+
* - Each shot's bottom layer, when it lasts exactly the shot, goes on a base track; otherwise the
|
|
6
|
+
* shot gets a black colour clip there. A shot's transition goes on its base clip (a provider
|
|
7
|
+
* `effect` transition from the base clip before), so it blends the base pictures.
|
|
8
|
+
* - Other layers go on tracks above by role (background, subject, overlay), packed so clips of a
|
|
9
|
+
* track never overlap: an effect with no subject is an effect clip; a picture or SVG is a media
|
|
10
|
+
* clip on a new image asset, carrying the layer's effect as a clip effect; words are a text clip;
|
|
11
|
+
* an open slot is a grey placeholder colour clip named after it.
|
|
12
|
+
* - A layer's `rect` becomes the clip's placement; `opacity` and `fade` carry over.
|
|
13
|
+
* - Titles and captions are text clips on the top track.
|
|
14
|
+
*
|
|
15
|
+
* Camera moves are not carried over (notes say so); set them with `set_clip_camera_move`.
|
|
16
|
+
*/
|
|
17
|
+
import type { EffectSequence } from '../effects/types.cjs';
|
|
18
|
+
import type { VideoEdit } from './edit.cjs';
|
|
19
|
+
import type { VideoProject } from './types.cjs';
|
|
20
|
+
export interface VideoSequenceEdits {
|
|
21
|
+
edits: VideoEdit[];
|
|
22
|
+
/** The new clips' ids, base clips first. */
|
|
23
|
+
clips: string[];
|
|
24
|
+
/** The new tracks' ids, top to bottom. */
|
|
25
|
+
tracks: string[];
|
|
26
|
+
assets: string[];
|
|
27
|
+
/** Where the sequence starts and ends on the timeline (ms). */
|
|
28
|
+
start: number;
|
|
29
|
+
end: number;
|
|
30
|
+
/** Placeholder clips standing in for open subject slots, by slot. */
|
|
31
|
+
placeholders: Record<string, string[]>;
|
|
32
|
+
notes: string[];
|
|
33
|
+
}
|
|
34
|
+
/** The edits that put a sequence on a project's timeline at `at` (default its end), on new tracks above the others. */
|
|
35
|
+
export declare function videoEditsFromSequence(project: VideoProject, sequence: EffectSequence, options?: {
|
|
36
|
+
at?: number;
|
|
37
|
+
provider?: string;
|
|
38
|
+
}): VideoSequenceEdits;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An effects provider's `EffectSequence` (what a sequence template fills to) as video edits, so a
|
|
3
|
+
* template lands on the timeline as one undo step:
|
|
4
|
+
*
|
|
5
|
+
* - Each shot's bottom layer, when it lasts exactly the shot, goes on a base track; otherwise the
|
|
6
|
+
* shot gets a black colour clip there. A shot's transition goes on its base clip (a provider
|
|
7
|
+
* `effect` transition from the base clip before), so it blends the base pictures.
|
|
8
|
+
* - Other layers go on tracks above by role (background, subject, overlay), packed so clips of a
|
|
9
|
+
* track never overlap: an effect with no subject is an effect clip; a picture or SVG is a media
|
|
10
|
+
* clip on a new image asset, carrying the layer's effect as a clip effect; words are a text clip;
|
|
11
|
+
* an open slot is a grey placeholder colour clip named after it.
|
|
12
|
+
* - A layer's `rect` becomes the clip's placement; `opacity` and `fade` carry over.
|
|
13
|
+
* - Titles and captions are text clips on the top track.
|
|
14
|
+
*
|
|
15
|
+
* Camera moves are not carried over (notes say so); set them with `set_clip_camera_move`.
|
|
16
|
+
*/
|
|
17
|
+
import type { EffectSequence } from '../effects/types.js';
|
|
18
|
+
import type { VideoEdit } from './edit.js';
|
|
19
|
+
import type { VideoProject } from './types.js';
|
|
20
|
+
export interface VideoSequenceEdits {
|
|
21
|
+
edits: VideoEdit[];
|
|
22
|
+
/** The new clips' ids, base clips first. */
|
|
23
|
+
clips: string[];
|
|
24
|
+
/** The new tracks' ids, top to bottom. */
|
|
25
|
+
tracks: string[];
|
|
26
|
+
assets: string[];
|
|
27
|
+
/** Where the sequence starts and ends on the timeline (ms). */
|
|
28
|
+
start: number;
|
|
29
|
+
end: number;
|
|
30
|
+
/** Placeholder clips standing in for open subject slots, by slot. */
|
|
31
|
+
placeholders: Record<string, string[]>;
|
|
32
|
+
notes: string[];
|
|
33
|
+
}
|
|
34
|
+
/** The edits that put a sequence on a project's timeline at `at` (default its end), on new tracks above the others. */
|
|
35
|
+
export declare function videoEditsFromSequence(project: VideoProject, sequence: EffectSequence, options?: {
|
|
36
|
+
at?: number;
|
|
37
|
+
provider?: string;
|
|
38
|
+
}): VideoSequenceEdits;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pixerate-rich-text
|
|
3
|
-
description: How to read and edit Pixerate rich text documents and prompt editors through agent tools - reading text with document positions (get_text, find_text), minimal-diff edits (replace_range, replace_text, insert_text), formatting (format_text for bold, italic, links and colour; set_block_type for headings, lists and quotes; set_text_align), selections, tables (edit_table and table.* commands), charts and sparklines embedded in a document, find and replace commands, and prompt editing with templates, variables and instructions (prompt_insert_text, prompt_replace_range, prompt_set_variable, get_editor_state, view_editor). Use when asked to write, rewrite, proofread, restructure or format a document, report, note, heading, paragraph or table, to insert a chart or sparkline into text, or to edit an image-generation or LLM prompt with {{template}}, {variable} and __instruction__ tokens.
|
|
3
|
+
description: How to read and edit Pixerate rich text documents and prompt editors through agent tools - reading text with document positions (get_text, find_text), minimal-diff edits (replace_range, replace_text, insert_text), formatting (format_text for bold, italic, links and colour; set_block_type for headings, lists and quotes; set_text_align), selections, tables (edit_table and table.* commands), charts and sparklines embedded in a document, comments and tracked changes (add_comment, list_changes, accept_change), find and replace commands, and prompt editing with templates, variables and instructions (prompt_insert_text, prompt_replace_range, prompt_set_variable, get_editor_state, view_editor). Use when asked to write, rewrite, proofread, restructure or format a document, report, note, heading, paragraph or table, to insert a chart or sparkline into text, to comment, suggest edits or accept changes, or to edit an image-generation or LLM prompt with {{template}}, {variable} and __instruction__ tokens.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Editing rich text documents and prompts
|
|
@@ -72,6 +72,13 @@ the pitfalls, and uses the tools below.
|
|
|
72
72
|
- **Charts in text.** Introduce a chart in the sentence before it and state the takeaway in
|
|
73
73
|
its title ("Revenue doubled in Q3", not "Revenue"). Use a sparkline for a trend inside a
|
|
74
74
|
sentence or table cell and give it a `label` that says what it shows.
|
|
75
|
+
- **Review.** When the user asks you to suggest edits rather than make them, call
|
|
76
|
+
`set_track_changes {enabled: true}` first so every write shows as a suggestion they can
|
|
77
|
+
accept or reject, and turn it back off only if it was off before. Comment (`add_comment`)
|
|
78
|
+
when a point needs the author's decision instead of a fix; keep each comment to one point and
|
|
79
|
+
say what you suggest. Resolve threads you have addressed rather than deleting them. Read
|
|
80
|
+
`list_changes` before accepting or rejecting, and act on ids one at a time unless the user
|
|
81
|
+
asked for all.
|
|
75
82
|
- **Prompt hygiene.** Put everything that changes between runs in a `{variable}` and set it
|
|
76
83
|
with `prompt_set_variable` rather than editing the text. Give variables descriptive
|
|
77
84
|
snake_case names and, where useful, a default and a description:
|
|
@@ -114,6 +121,26 @@ the pitfalls, and uses the tools below.
|
|
|
114
121
|
| `prompt_set_variable` | Set the value of a template variable (`name` without braces, `value` any JSON value). Prefer this over editing the prompt text when only a slot's value changes. |
|
|
115
122
|
| `prompt_select` | Select prompt text between two positions (`from === to` places the cursor), e.g. to show the user what you changed. |
|
|
116
123
|
|
|
124
|
+
### Review
|
|
125
|
+
|
|
126
|
+
Present when the editor has the `Comments` and / or `TrackChanges` extensions (the host may give
|
|
127
|
+
them their own namespace, e.g. `review__list_comments`). Positions are document positions, as
|
|
128
|
+
above. The write tools are hidden while the document is read-only.
|
|
129
|
+
|
|
130
|
+
| Tool | Use it to |
|
|
131
|
+
| --- | --- |
|
|
132
|
+
| `list_comments` | List comment threads in document order: `id`, the commented `from` / `to` and `quote`, `author`, `body`, `replies` (`id`, `author`, `body`), `resolved`, `orphaned` (its text was deleted; `from` / `to` are null). `includeResolved` default true. Call before replying, resolving or deleting. |
|
|
133
|
+
| `add_comment` | Comment on `from`..`to` (from `find_text`) with a `body`. One undo step; the selection moves to the commented text. Returns the new thread with its `id`. Fails on an empty or out-of-range span. |
|
|
134
|
+
| `reply_to_comment` | Reply to a thread by `commentId` with a `body`. Re-opens a resolved thread. Returns `replyId` and the thread. |
|
|
135
|
+
| `resolve_comment` | Resolve a thread by `commentId` (`resolved: false` re-opens it). Keeps the thread and its text. Returns the thread. Prefer this over deleting once a point is addressed. |
|
|
136
|
+
| `delete_comment` | Delete a thread and its replies and remove its highlight (one undo step). Returns `deleted` and how many remain. Only when the user asks, or for a comment you added by mistake. |
|
|
137
|
+
| `list_changes` | List tracked changes in document order: `id`, `type` (`insertion`, `deletion`, `format`, `block`, `move`), `author`, `from` / `to`, `text`, a plain-language `description` (moves add `movedFrom` / `movedTo`), and `trackingEnabled`. `type` filters. Call before accepting or rejecting. |
|
|
138
|
+
| `accept_change` | Accept one change by `changeId` (one undo step): inserted text stays, deleted text goes, a move keeps its destination. Returns the change and how many `remaining`. Ids of other changes stay valid. |
|
|
139
|
+
| `reject_change` | Reject one change by `changeId` (one undo step): inserted text goes, deleted text and old formatting come back, a move returns to its source. Returns the change and `remaining`. |
|
|
140
|
+
| `accept_all_changes` | Accept every tracked change in one undo step. Returns how many. Only when the user asks for all; otherwise go one by one. |
|
|
141
|
+
| `reject_all_changes` | Reject every tracked change in one undo step. Returns how many. |
|
|
142
|
+
| `set_track_changes` | Turn tracking on (`enabled: true`: every later edit, yours included, becomes a suggestion) or off. Returns `trackingEnabled`. Turn it on before suggesting edits. |
|
|
143
|
+
|
|
117
144
|
### Save and open
|
|
118
145
|
|
|
119
146
|
The host may add the shared document tools (see `pixerate-agent-tools`). A rich text document is the editor's ProseMirror JSON (`{ "type": "doc", "content": [...] }`), with marks such as tracked changes and comment anchors; comment threads stay in their own store.
|
|
@@ -29,6 +29,9 @@ This skill is the method. `pixerate-rich-text` has the full tool reference.
|
|
|
29
29
|
|
|
30
30
|
- **Scope:** spelling only, grammar and style, or a rewrite of named passages.
|
|
31
31
|
- **Exceptions:** text that must stay as written (quotes, legal text, product names, titles).
|
|
32
|
+
- **Edit or suggest:** when the user wants to review your changes, turn on `set_track_changes`
|
|
33
|
+
first so each fix is a suggestion, and flag doubtful points with `add_comment` instead of
|
|
34
|
+
changing them.
|
|
32
35
|
- **House style:** spelling variant (UK or US), how numbers and dates are written.
|
|
33
36
|
|
|
34
37
|
## Working method
|
|
@@ -47,7 +50,8 @@ This skill is the method. `pixerate-rich-text` has the full tool reference.
|
|
|
47
50
|
**Check:** `get_text` `blocks`: the other blocks' text is unchanged.
|
|
48
51
|
5. **Consistency.** Terms, numbers and dates one way: `find_text` with a `regex` for the
|
|
49
52
|
variants, then `replace_text`.
|
|
50
|
-
6. **Report** the fixes made, the matches you left and why.
|
|
53
|
+
6. **Report** the fixes made, the matches you left and why. When tracking is on, `list_changes`
|
|
54
|
+
shows the suggestions you made; leave accepting them to the user.
|
|
51
55
|
|
|
52
56
|
## Decision table
|
|
53
57
|
|
|
@@ -58,6 +62,7 @@ This skill is the method. `pixerate-rich-text` has the full tool reference.
|
|
|
58
62
|
| A family of variants (e-mail, email, E-mail) | `find_text` / `replace_text` with `regex: true` |
|
|
59
63
|
| Rewrite a sentence or paragraph | `replace_range` over its exact range |
|
|
60
64
|
| Insert a missing word | `insert_text` at the position from `find_text` |
|
|
65
|
+
| A point only the author can decide | `add_comment` on the passage, saying what you suggest |
|
|
61
66
|
| Formatting only | `format_text` on the range; leave the words |
|
|
62
67
|
|
|
63
68
|
## Recipes
|
|
@@ -90,3 +95,5 @@ blocks[0].to, text }` with one sentence.
|
|
|
90
95
|
- `replace_range`, `insert_text`: rewrite or insert at exact positions
|
|
91
96
|
- `format_text`: formatting fixes without touching words
|
|
92
97
|
- `undo_rich_text`: take back a step
|
|
98
|
+
- `set_track_changes`, `list_changes`: suggest fixes as tracked changes and list them
|
|
99
|
+
- `add_comment`: flag what the author should decide
|