@plannotator/ui 0.31.0 β 0.33.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/HANDOFF.md +664 -0
- package/README.md +57 -1
- package/components/AnnotationPanel.tsx +96 -4
- package/components/AnnotationToolbar.tsx +25 -25
- package/components/CommentPopover.tsx +26 -0
- package/components/GraphvizBlock.tsx +86 -7
- package/components/HtmlSurfaceControls.tsx +170 -0
- package/components/InlineMarkdown.tsx +22 -2
- package/components/MermaidBlock.tsx +60 -26
- package/components/Settings.tsx +40 -1
- package/components/blocks/MathBlock.tsx +26 -14
- package/components/html-viewer/HtmlViewer.tsx +289 -6
- package/components/html-viewer/bridge-script.asset.js +4392 -0
- package/components/html-viewer/bridge-script.lite.ts +9 -0
- package/components/html-viewer/bridge-script.ts +54 -8
- package/components/html-viewer/hostThreads.ts +37 -0
- package/components/html-viewer/index.ts +22 -1
- package/components/html-viewer/srcdoc.ts +70 -1
- package/components/html-viewer/unanchored.ts +47 -0
- package/components/html-viewer/useHtmlAnnotation.ts +132 -5
- package/configure.ts +32 -0
- package/hooks/useHtmlRefresh.ts +149 -0
- package/hooks/useMathRenderer.ts +30 -0
- package/hooks/useSharing.ts +31 -5
- package/package.json +9 -3
- package/styles.css +1 -1
- package/types.ts +1 -0
- package/utils/generateIdentity.ts +64 -14
- package/utils/identity-tater.ts +36 -0
- package/utils/math-default-loader.ts +24 -0
- package/utils/math-eager.ts +25 -0
- package/utils/math.ts +149 -0
- package/utils/mermaid-eager.ts +28 -0
- package/utils/mermaid.ts +132 -0
- package/utils/parser.ts +38 -0
- package/utils/quickLabels.ts +13 -0
- package/webmcp/activity.ts +46 -0
- package/webmcp/changes.ts +227 -0
- package/webmcp/index.ts +72 -0
- package/webmcp/modelContext.ts +103 -0
- package/webmcp/nudges.ts +174 -0
- package/webmcp/policy.ts +50 -0
- package/webmcp/preference.ts +50 -0
- package/webmcp/schema.ts +81 -0
- package/webmcp/toolset.ts +337 -0
- package/webmcp/useToolset.ts +74 -0
package/types.ts
CHANGED
|
@@ -80,6 +80,7 @@ export interface Annotation {
|
|
|
80
80
|
}>; // math elements covered by a mixed text+formula selection
|
|
81
81
|
prUrl?: string; // code-review PR mode: the PR this note belongs to, so it isn't shown/exported against another PR after an in-place switch
|
|
82
82
|
pageUrl?: string; // set only by live app annotate sessions: the page (pathname + search) the annotation was made on; restore filters to the current page and export groups by page
|
|
83
|
+
inReplyTo?: string; // id of the annotation this one replies to; a reply inherits its parent's anchor, renders indented under it in the panel, and exports grouped under it. Additive: annotations without it render and export exactly as before.
|
|
83
84
|
htmlAnchor?: HtmlElementAnchor; // raw-HTML pinpoint: serialized element anchor for reliable restoration
|
|
84
85
|
htmlAdditionalTargets?: HtmlAnnotationTarget[]; // raw-HTML shift-click multi-select: extra elements this one comment covers (primary stays htmlAnchor/originalText)
|
|
85
86
|
// web-highlighter metadata for cross-element selections
|
|
@@ -1,12 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tater name generator
|
|
2
|
+
* Tater name generator: pure function, no storage dependencies.
|
|
3
3
|
*
|
|
4
4
|
* Extracted to its own module to avoid circular imports:
|
|
5
5
|
* settings.ts needs this for the default value, and identity.ts
|
|
6
6
|
* needs configStore (which imports settings.ts).
|
|
7
|
+
*
|
|
8
|
+
* The full `unique-username-generator` dictionary is NOT imported here. It is
|
|
9
|
+
* registered into the slot below by `./identity-tater`, which every
|
|
10
|
+
* Plannotator entry imports eagerly, so Plannotator mints names from the
|
|
11
|
+
* full dictionary exactly as before. A host that provides its own
|
|
12
|
+
* `identityProvider` never calls the generator and, with the static import
|
|
13
|
+
* gone, no longer ships the word lists.
|
|
14
|
+
*
|
|
15
|
+
* The slot is SYNCHRONOUS on purpose: `configStore.ensureLoaded()` evaluates
|
|
16
|
+
* this default during the first settings read (a render-time read) and
|
|
17
|
+
* persists the result to the identity cookie at once, so a name that arrived
|
|
18
|
+
* later would be a visible identity change. The built-in fallback below keeps
|
|
19
|
+
* the same `{adjective}-{noun}-tater` shape from a small inline pool.
|
|
7
20
|
*/
|
|
8
21
|
|
|
9
|
-
|
|
22
|
+
export type IdentityGenerator = () => string;
|
|
23
|
+
|
|
24
|
+
const FALLBACK_ADJECTIVES = [
|
|
25
|
+
'swift', 'gentle', 'brave', 'calm', 'clever', 'bright', 'quiet', 'bold',
|
|
26
|
+
'eager', 'kind', 'lucky', 'merry', 'nimble', 'proud', 'sunny', 'witty',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
const FALLBACK_NOUNS = [
|
|
30
|
+
'falcon', 'crystal', 'river', 'meadow', 'harbor', 'comet', 'maple', 'otter',
|
|
31
|
+
'summit', 'lantern', 'willow', 'ember', 'pebble', 'breeze', 'orchid', 'canyon',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/** Pool words: exported for tests only. */
|
|
35
|
+
export const FALLBACK_IDENTITY_POOL: {
|
|
36
|
+
readonly adjectives: readonly string[];
|
|
37
|
+
readonly nouns: readonly string[];
|
|
38
|
+
} = {
|
|
39
|
+
adjectives: FALLBACK_ADJECTIVES,
|
|
40
|
+
nouns: FALLBACK_NOUNS,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function pick<T>(list: readonly T[]): T {
|
|
44
|
+
return list[Math.floor(Math.random() * list.length)]!;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Built-in generator: same shape as the dictionary one, from a 16 x 16 pool. */
|
|
48
|
+
export const fallbackIdentityGenerator: IdentityGenerator = () =>
|
|
49
|
+
`${pick(FALLBACK_ADJECTIVES)}-${pick(FALLBACK_NOUNS)}-tater`;
|
|
50
|
+
|
|
51
|
+
let generator: IdentityGenerator = fallbackIdentityGenerator;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Register the generator `generateIdentity()` delegates to. Must return a
|
|
55
|
+
* string synchronously. `./identity-tater` registers the full dictionary;
|
|
56
|
+
* a host may register its own via `configurePlannotatorUI({ identityGenerator })`.
|
|
57
|
+
*/
|
|
58
|
+
export function setIdentityGenerator(next: IdentityGenerator): void {
|
|
59
|
+
generator = next;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The active generator. Exported so a test can assert which one is registered. */
|
|
63
|
+
export function getIdentityGenerator(): IdentityGenerator {
|
|
64
|
+
return generator;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reset to the built-in fallback pool. Mainly for tests. */
|
|
68
|
+
export function resetIdentityGenerator(): void {
|
|
69
|
+
generator = fallbackIdentityGenerator;
|
|
70
|
+
}
|
|
10
71
|
|
|
11
72
|
/**
|
|
12
73
|
* Generate a new random tater identity.
|
|
@@ -14,16 +75,5 @@ import { uniqueUsernameGenerator, adjectives, nouns } from 'unique-username-gene
|
|
|
14
75
|
* Examples: "swift-falcon-tater", "gentle-crystal-tater"
|
|
15
76
|
*/
|
|
16
77
|
export function generateIdentity(): string {
|
|
17
|
-
|
|
18
|
-
// with compound words that contain hyphens (e.g., "behind-the-scenes")
|
|
19
|
-
const generated = uniqueUsernameGenerator({
|
|
20
|
-
dictionaries: [adjectives, nouns],
|
|
21
|
-
separator: '|||',
|
|
22
|
-
style: 'lowerCase',
|
|
23
|
-
randomDigits: 0,
|
|
24
|
-
length: 50, // Prevent word truncation (default is too short)
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const [adjective, noun] = generated.split('|||');
|
|
28
|
-
return `${adjective}-${noun}-tater`;
|
|
78
|
+
return generator();
|
|
29
79
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eager identity registration: installs the full `unique-username-generator`
|
|
3
|
+
* dictionary into the generator slot in `./generateIdentity` at module
|
|
4
|
+
* evaluation, before any settings read can mint a name.
|
|
5
|
+
*
|
|
6
|
+
* Every Plannotator entry (`packages/editor/App.tsx`, `packages/review-editor/App.tsx`)
|
|
7
|
+
* imports this module for its side effect, which keeps Plannotator's tater
|
|
8
|
+
* names byte-identical to before: same library, same config, same call. A host
|
|
9
|
+
* that wants the full dictionary without its own identity provider imports it
|
|
10
|
+
* too:
|
|
11
|
+
*
|
|
12
|
+
* import '@plannotator/ui/utils/identity-tater';
|
|
13
|
+
*
|
|
14
|
+
* A host that provides `identityProvider` never calls the generator and should
|
|
15
|
+
* NOT import this, so the word lists stay out of its bundle.
|
|
16
|
+
*/
|
|
17
|
+
import { uniqueUsernameGenerator, adjectives, nouns } from 'unique-username-generator';
|
|
18
|
+
import { setIdentityGenerator, type IdentityGenerator } from './generateIdentity';
|
|
19
|
+
|
|
20
|
+
/** The dictionary generator Plannotator has always used. */
|
|
21
|
+
export const generateTaterIdentity: IdentityGenerator = () => {
|
|
22
|
+
// Use a unique separator to split adjective from noun, avoiding issues
|
|
23
|
+
// with compound words that contain hyphens (e.g., "behind-the-scenes")
|
|
24
|
+
const generated = uniqueUsernameGenerator({
|
|
25
|
+
dictionaries: [adjectives, nouns],
|
|
26
|
+
separator: '|||',
|
|
27
|
+
style: 'lowerCase',
|
|
28
|
+
randomDigits: 0,
|
|
29
|
+
length: 50, // Prevent word truncation (default is too short)
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const [adjective, noun] = generated.split('|||');
|
|
33
|
+
return `${adjective}-${noun}-tater`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
setIdentityGenerator(generateTaterIdentity);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The default math renderer loader: KaTeX's JS only, fetched lazily.
|
|
3
|
+
*
|
|
4
|
+
* This module is the ONLY place in `@plannotator/ui` that names `katex` at
|
|
5
|
+
* runtime (`./math-eager` names it too, but a host chooses to import that).
|
|
6
|
+
* `./math` calls `loadDefaultMathRenderer` only when no host loader is
|
|
7
|
+
* registered (`setMathRendererLoader` / `configurePlannotatorUI({
|
|
8
|
+
* mathRendererLoader })`), so a host that registers one never runs the
|
|
9
|
+
* `import('katex')` below and never requests the chunk it produces.
|
|
10
|
+
*
|
|
11
|
+
* Keeping the import in its own module is what lets a bundler drop the chunk
|
|
12
|
+
* entirely: chunk emission is static, so a host that registers a loader and
|
|
13
|
+
* wants no KaTeX chunk from the package at all points this module at a stub
|
|
14
|
+
* (see HANDOFF.md "Lazy renderers and eager entries", the alias recipe). The
|
|
15
|
+
* stylesheet is deliberately NOT imported here; CSS loading stays the host's
|
|
16
|
+
* job (HANDOFF.md "Math rendering"), and a host that already serves
|
|
17
|
+
* `katex.min.css` would otherwise load it twice.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { MathRenderer } from './math';
|
|
21
|
+
|
|
22
|
+
export function loadDefaultMathRenderer(): Promise<MathRenderer> {
|
|
23
|
+
return import('katex').then((m) => m.default);
|
|
24
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eager math registration: fills the renderer slot in `./math` with KaTeX at
|
|
3
|
+
* module evaluation, before any component renders.
|
|
4
|
+
*
|
|
5
|
+
* Every Plannotator entry (`packages/editor/App.tsx`, `packages/review-editor/App.tsx`;
|
|
6
|
+
* the hook, review, portal, OpenCode and Pi builds all flow from those two)
|
|
7
|
+
* imports this module for its side effect, which is what keeps math typeset
|
|
8
|
+
* on the first commit exactly as it was with a static `katex` import. A host
|
|
9
|
+
* that wants the same synchronous behavior imports it too:
|
|
10
|
+
*
|
|
11
|
+
* import '@plannotator/ui/utils/math-eager';
|
|
12
|
+
*
|
|
13
|
+
* A host that does not import it gets lazy math: the TeX source in the same
|
|
14
|
+
* wrapper for one frame, then the typeset markup once the chunk lands.
|
|
15
|
+
*
|
|
16
|
+
* The source tag passed below doubles as a build marker: the literal only
|
|
17
|
+
* reaches a bundle when this module is evaluated in it, so a dropped or
|
|
18
|
+
* tree-shaken side-effect import is caught by the built-HTML check in
|
|
19
|
+
* tests/entry-assets.test.ts (KaTeX itself stays inlined either way through
|
|
20
|
+
* the loader's import(), so a KaTeX class name cannot prove registration).
|
|
21
|
+
*/
|
|
22
|
+
import katex from 'katex';
|
|
23
|
+
import { setMathRenderer } from './math';
|
|
24
|
+
|
|
25
|
+
setMathRenderer(katex, 'plannotator-math-eager');
|
package/utils/math.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Math renderer slot.
|
|
3
|
+
*
|
|
4
|
+
* `MathBlock` and inline math read their renderer from this module instead of
|
|
5
|
+
* importing `katex` statically, so a host that bundles by route does not carry
|
|
6
|
+
* KaTeX in every document read. The slot is SYNCHRONOUS: when it is filled
|
|
7
|
+
* before the first render (which is what `./math-eager` does, and what every
|
|
8
|
+
* Plannotator entry imports) the typeset HTML is in the DOM on the first
|
|
9
|
+
* commit, exactly as it was when the import was static. When it is empty the
|
|
10
|
+
* components render the same wrapper element with the TeX source as text,
|
|
11
|
+
* call `loadMathRenderer()`, and re-render typeset once it resolves.
|
|
12
|
+
*
|
|
13
|
+
* This module deliberately has NO runtime import of `katex`: the only place
|
|
14
|
+
* the dependency is named is `./math-default-loader`'s `import('katex')`,
|
|
15
|
+
* which a chunking bundler turns into a lazy chunk and Plannotator's
|
|
16
|
+
* single-file builds inline (the eager entry keeps it in the entry either
|
|
17
|
+
* way). That default is called only while no host loader is registered, and
|
|
18
|
+
* it lives in its own module so a host that registers a loader can alias it
|
|
19
|
+
* away and drop the chunk (see HANDOFF.md "Lazy renderers and eager entries").
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { KatexOptions } from 'katex';
|
|
23
|
+
import { loadDefaultMathRenderer } from './math-default-loader';
|
|
24
|
+
|
|
25
|
+
/** The subset of KaTeX's API the renderer needs. `katex` itself satisfies it. */
|
|
26
|
+
export interface MathRenderer {
|
|
27
|
+
renderToString(tex: string, options?: KatexOptions): string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type MathRendererLoader = () => Promise<MathRenderer>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Who filled the slot: the eager entry (`./math-eager`), the lazy loader, or a
|
|
34
|
+
* host calling `setMathRenderer` directly. Diagnostic for a host chasing a TeX
|
|
35
|
+
* flash, and the eager value is a build marker: it only reaches a bundle when
|
|
36
|
+
* `./math-eager` is evaluated, which is what `tests/entry-assets.test.ts`
|
|
37
|
+
* asserts on the built single-file HTML.
|
|
38
|
+
*/
|
|
39
|
+
export type MathRendererSource = 'plannotator-math-eager' | 'loader' | 'host';
|
|
40
|
+
|
|
41
|
+
let renderer: MathRenderer | null = null;
|
|
42
|
+
let rendererSource: MathRendererSource | null = null;
|
|
43
|
+
/**
|
|
44
|
+
* The host loader, or `null` while none is registered. `null` is the only
|
|
45
|
+
* state in which `loadMathRenderer()` reaches `loadDefaultMathRenderer` and
|
|
46
|
+
* its `import('katex')`; a registered loader is never backfilled by the
|
|
47
|
+
* default, not even after it rejects.
|
|
48
|
+
*/
|
|
49
|
+
let loader: MathRendererLoader | null = null;
|
|
50
|
+
let pending: Promise<MathRenderer> | null = null;
|
|
51
|
+
const listeners = new Set<() => void>();
|
|
52
|
+
|
|
53
|
+
function notify(): void {
|
|
54
|
+
for (const listener of listeners) listener();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Current renderer, or `null` while none is registered. Safe to call during render. */
|
|
58
|
+
export function getMathRenderer(): MathRenderer | null {
|
|
59
|
+
return renderer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** How the current renderer was registered, or `null` while the slot is empty. */
|
|
63
|
+
export function getMathRendererSource(): MathRendererSource | null {
|
|
64
|
+
return rendererSource;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Register a renderer synchronously (what `./math-eager` does with `katex`). */
|
|
68
|
+
export function setMathRenderer(next: MathRenderer, source: MathRendererSource = 'host'): void {
|
|
69
|
+
if (renderer === next) return;
|
|
70
|
+
renderer = next;
|
|
71
|
+
rendererSource = source;
|
|
72
|
+
notify();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Subscribe to slot changes. Shaped for `useSyncExternalStore`. */
|
|
76
|
+
export function subscribeMathRenderer(listener: () => void): () => void {
|
|
77
|
+
listeners.add(listener);
|
|
78
|
+
return () => {
|
|
79
|
+
listeners.delete(listener);
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Swap the loader `loadMathRenderer()` uses. Host seam
|
|
85
|
+
* (`configurePlannotatorUI({ mathRendererLoader })`): a host may return a
|
|
86
|
+
* module that imports katex AND its stylesheet in one chunk. A load already in
|
|
87
|
+
* flight keeps going; the new loader is used from the next `loadMathRenderer()`
|
|
88
|
+
* call that finds the slot empty.
|
|
89
|
+
*/
|
|
90
|
+
export function setMathRendererLoader(next: MathRendererLoader): void {
|
|
91
|
+
loader = next;
|
|
92
|
+
pending = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Load and register the renderer. Idempotent: a filled slot resolves at once,
|
|
97
|
+
* a load in flight is shared, and a rejected load is dropped so the next call
|
|
98
|
+
* retries instead of failing forever on a transient chunk error.
|
|
99
|
+
*/
|
|
100
|
+
export function loadMathRenderer(): Promise<MathRenderer> {
|
|
101
|
+
if (renderer) return Promise.resolve(renderer);
|
|
102
|
+
if (!pending) {
|
|
103
|
+
const attempt = (loader ? loader() : loadDefaultMathRenderer()).then(
|
|
104
|
+
(loaded) => {
|
|
105
|
+
setMathRenderer(loaded, 'loader');
|
|
106
|
+
return loaded;
|
|
107
|
+
},
|
|
108
|
+
(err: unknown) => {
|
|
109
|
+
if (pending === attempt) pending = null;
|
|
110
|
+
throw err;
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
pending = attempt;
|
|
114
|
+
}
|
|
115
|
+
return pending;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Test hook: clear the slot, the loader override and any pending load. */
|
|
119
|
+
export function resetMathRenderer(): void {
|
|
120
|
+
renderer = null;
|
|
121
|
+
rendererSource = null;
|
|
122
|
+
loader = null;
|
|
123
|
+
pending = null;
|
|
124
|
+
notify();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const normalizeMathTex = (tex: string): string => tex.trim();
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Render TeX with the pinned option set. `throwOnError: false` and
|
|
131
|
+
* `trust: false` are a deliberate security pin applied to EVERY renderer,
|
|
132
|
+
* including one a host registered: a registered module never widens what
|
|
133
|
+
* document-supplied TeX may do. Returns `null` while no renderer is
|
|
134
|
+
* registered so callers can fall back to the text placeholder.
|
|
135
|
+
*/
|
|
136
|
+
export function renderMathToHtml(
|
|
137
|
+
tex: string,
|
|
138
|
+
displayMode: boolean,
|
|
139
|
+
activeRenderer: MathRenderer | null = renderer,
|
|
140
|
+
): string | null {
|
|
141
|
+
if (!activeRenderer) return null;
|
|
142
|
+
return activeRenderer.renderToString(tex, {
|
|
143
|
+
displayMode,
|
|
144
|
+
throwOnError: false,
|
|
145
|
+
strict: 'warn',
|
|
146
|
+
trust: false,
|
|
147
|
+
output: 'html',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eager Mermaid registration: imports the runtime statically, initializes it
|
|
3
|
+
* at module evaluation (exactly where the old module-scope
|
|
4
|
+
* `mermaid.initialize` ran) and fills the slot in `./mermaid`.
|
|
5
|
+
*
|
|
6
|
+
* `packages/editor/App.tsx` imports this module for its side effect, by
|
|
7
|
+
* policy: Plannotator's own surfaces keep Mermaid in their entry chunk so it
|
|
8
|
+
* can never fail separately from the app (on the share portal this is what
|
|
9
|
+
* keeps `mermaid.core` out of a lazy chunk). The review editor does not import
|
|
10
|
+
* it because it never renders a Mermaid block; adding the runtime there would
|
|
11
|
+
* grow that bundle. A host that wants the same import adds:
|
|
12
|
+
*
|
|
13
|
+
* import '@plannotator/ui/utils/mermaid-eager';
|
|
14
|
+
*
|
|
15
|
+
* A host that does not import it gets the lazy path in `./mermaid`.
|
|
16
|
+
*
|
|
17
|
+
* The source tag passed below doubles as a build marker: the literal only
|
|
18
|
+
* reaches a bundle when this module is evaluated in it, so a dropped or
|
|
19
|
+
* tree-shaken side-effect import is caught by the built-HTML check in
|
|
20
|
+
* tests/entry-assets.test.ts (the runtime itself stays inlined in a
|
|
21
|
+
* single-file build through the loader's import(), so a Mermaid diagram id
|
|
22
|
+
* cannot prove registration).
|
|
23
|
+
*/
|
|
24
|
+
import mermaid from 'mermaid';
|
|
25
|
+
import { MERMAID_CONFIG, setMermaidRuntime } from './mermaid';
|
|
26
|
+
|
|
27
|
+
mermaid.initialize(MERMAID_CONFIG);
|
|
28
|
+
setMermaidRuntime(mermaid, 'plannotator-mermaid-eager');
|
package/utils/mermaid.ts
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mermaid runtime slot.
|
|
3
|
+
*
|
|
4
|
+
* ONE code path feeds `MermaidBlock`: `loadMermaidRuntime()`. It resolves at
|
|
5
|
+
* once from a filled slot and otherwise imports the runtime lazily. Plannotator
|
|
6
|
+
* fills the slot at module evaluation through `./mermaid-eager` (imported by
|
|
7
|
+
* `packages/editor/App.tsx`), which keeps the runtime in its entry chunk on the
|
|
8
|
+
* share portal exactly as it was with the static import, so it cannot fail
|
|
9
|
+
* separately from the app. A host that does not import the eager entry gets
|
|
10
|
+
* the lazy path: the runtime is fetched on the first diagram, a failed import
|
|
11
|
+
* is dropped from the memo so the next call issues a fresh `import()`, and
|
|
12
|
+
* the block re-attempts once and offers Retry.
|
|
13
|
+
*
|
|
14
|
+
* This module has NO static import of `mermaid`; the only place the
|
|
15
|
+
* dependency is named at runtime is the default loader's `import('mermaid')`.
|
|
16
|
+
*/
|
|
17
|
+
import type { Mermaid, MermaidConfig } from 'mermaid';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hoisted verbatim from the former module-scope `mermaid.initialize(...)` in
|
|
21
|
+
* MermaidBlock. Nothing in it reads a CSS token or the resolved mode.
|
|
22
|
+
* `securityLevel: 'strict'` is a deliberate security pin (see MermaidBlock.test.ts).
|
|
23
|
+
*/
|
|
24
|
+
export const MERMAID_CONFIG: MermaidConfig = {
|
|
25
|
+
startOnLoad: false,
|
|
26
|
+
securityLevel: 'strict',
|
|
27
|
+
theme: 'dark',
|
|
28
|
+
themeVariables: {
|
|
29
|
+
primaryColor: '#3b82f6',
|
|
30
|
+
primaryTextColor: '#f8fafc',
|
|
31
|
+
primaryBorderColor: '#475569',
|
|
32
|
+
lineColor: '#64748b',
|
|
33
|
+
secondaryColor: '#1e293b',
|
|
34
|
+
tertiaryColor: '#0f172a',
|
|
35
|
+
background: '#1e293b',
|
|
36
|
+
mainBkg: '#1e293b',
|
|
37
|
+
nodeBorder: '#475569',
|
|
38
|
+
clusterBkg: '#1e293b',
|
|
39
|
+
clusterBorder: '#475569',
|
|
40
|
+
titleColor: '#f8fafc',
|
|
41
|
+
edgeLabelBackground: '#1e293b',
|
|
42
|
+
},
|
|
43
|
+
flowchart: {
|
|
44
|
+
htmlLabels: true,
|
|
45
|
+
curve: 'basis',
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Who filled the slot. The eager value doubles as a build marker: the literal
|
|
51
|
+
* only reaches a bundle when `./mermaid-eager` is evaluated in it, which is
|
|
52
|
+
* what `tests/entry-assets.test.ts` asserts on the built HTML.
|
|
53
|
+
*/
|
|
54
|
+
export type MermaidRuntimeSource = 'plannotator-mermaid-eager' | 'loader' | 'host';
|
|
55
|
+
|
|
56
|
+
export type MermaidRuntimeLoader = () => Promise<Mermaid>;
|
|
57
|
+
|
|
58
|
+
/** Default lazy loader: import the runtime and initialize it once. */
|
|
59
|
+
const defaultMermaidLoader: MermaidRuntimeLoader = () =>
|
|
60
|
+
import('mermaid').then(({ default: mermaid }) => {
|
|
61
|
+
mermaid.initialize(MERMAID_CONFIG);
|
|
62
|
+
return mermaid;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
let runtime: Mermaid | null = null;
|
|
66
|
+
let runtimeSource: MermaidRuntimeSource | null = null;
|
|
67
|
+
let loader: MermaidRuntimeLoader = defaultMermaidLoader;
|
|
68
|
+
let pending: Promise<Mermaid> | null = null;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Delay before the block's one automatic re-attempt after a failed lazy
|
|
72
|
+
* import. Only chunking hosts can fail here; a filled slot never loads.
|
|
73
|
+
*/
|
|
74
|
+
let retryDelayMs = 750;
|
|
75
|
+
|
|
76
|
+
/** Current runtime, or `null` while the slot is empty. */
|
|
77
|
+
export function getMermaidRuntime(): Mermaid | null {
|
|
78
|
+
return runtime;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** How the current runtime was registered, or `null` while the slot is empty. */
|
|
82
|
+
export function getMermaidRuntimeSource(): MermaidRuntimeSource | null {
|
|
83
|
+
return runtimeSource;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Register an already-initialized runtime (what `./mermaid-eager` does). */
|
|
87
|
+
export function setMermaidRuntime(next: Mermaid, source: MermaidRuntimeSource = 'host'): void {
|
|
88
|
+
runtime = next;
|
|
89
|
+
runtimeSource = source;
|
|
90
|
+
pending = null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The block's retry delay for the lazy path. */
|
|
94
|
+
export function getMermaidRetryDelayMs(): number {
|
|
95
|
+
return retryDelayMs;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve the runtime: at once from a filled slot, otherwise through the
|
|
100
|
+
* loader. A rejected load is dropped from the memo so the next call (the
|
|
101
|
+
* block's automatic re-attempt, a later mount, or the Retry button) issues a
|
|
102
|
+
* fresh `import()` instead of replaying the cached rejection.
|
|
103
|
+
*/
|
|
104
|
+
export function loadMermaidRuntime(): Promise<Mermaid> {
|
|
105
|
+
if (runtime) return Promise.resolve(runtime);
|
|
106
|
+
if (!pending) {
|
|
107
|
+
const attempt = loader().then(
|
|
108
|
+
(loaded) => {
|
|
109
|
+
setMermaidRuntime(loaded, 'loader');
|
|
110
|
+
return loaded;
|
|
111
|
+
},
|
|
112
|
+
(err: unknown) => {
|
|
113
|
+
if (pending === attempt) pending = null;
|
|
114
|
+
throw err;
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
pending = attempt;
|
|
118
|
+
}
|
|
119
|
+
return pending;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Test hook: empty the slot, stand in for the lazy import, shorten the retry delay. */
|
|
123
|
+
export function __setMermaidRuntimeLoaderForTests(
|
|
124
|
+
next: MermaidRuntimeLoader | undefined,
|
|
125
|
+
options?: { retryDelayMs?: number },
|
|
126
|
+
): void {
|
|
127
|
+
runtime = null;
|
|
128
|
+
runtimeSource = null;
|
|
129
|
+
pending = null;
|
|
130
|
+
loader = next ?? defaultMermaidLoader;
|
|
131
|
+
retryDelayMs = options?.retryDelayMs ?? 750;
|
|
132
|
+
}
|
package/utils/parser.ts
CHANGED
|
@@ -1208,6 +1208,38 @@ export const exportAnnotations = (
|
|
|
1208
1208
|
];
|
|
1209
1209
|
}
|
|
1210
1210
|
|
|
1211
|
+
// Threaded replies (`inReplyTo`): a reply is emitted as a nested exchange
|
|
1212
|
+
// under its parent's entry rather than as its own numbered entry, so the
|
|
1213
|
+
// coding agent reads the conversation in order. Replies whose parent is
|
|
1214
|
+
// not in the export render as ordinary entries. With no `inReplyTo`
|
|
1215
|
+
// anywhere the output is byte-identical to the ungrouped export.
|
|
1216
|
+
const exportedIds = new Set(sortedAnns.map((a: any) => a.id));
|
|
1217
|
+
const isReply = (a: any) => typeof a.inReplyTo === 'string' && a.inReplyTo !== a.id && exportedIds.has(a.inReplyTo);
|
|
1218
|
+
const hasReplies = sortedAnns.some(isReply);
|
|
1219
|
+
const repliesOf = (parent: any): any[] =>
|
|
1220
|
+
hasReplies ? sortedAnns.filter((a: any) => isReply(a) && a.inReplyTo === parent.id).sort((a: any, b: any) => a.createdA - b.createdA) : [];
|
|
1221
|
+
if (hasReplies) {
|
|
1222
|
+
emitOrder = emitOrder.filter((a) => !isReply(a));
|
|
1223
|
+
// Numbers stay consecutive over the entries that are actually emitted.
|
|
1224
|
+
annotationNumbers.clear();
|
|
1225
|
+
emitOrder.forEach((ann, index) => annotationNumbers.set(ann, index + 1));
|
|
1226
|
+
}
|
|
1227
|
+
const replyBlock = (parent: any, depth = 0): string => {
|
|
1228
|
+
let block = '';
|
|
1229
|
+
for (const reply of repliesOf(parent)) {
|
|
1230
|
+
const who = reply.author ? `${reply.author}` : 'reply';
|
|
1231
|
+
const indent = ' '.repeat(depth);
|
|
1232
|
+
block += `${indent}- **Reply (${who}):** ${String(reply.text ?? '').replace(/\r?\n/g, `\n${indent} `)}\n`;
|
|
1233
|
+
if (reply.images && reply.images.length > 0) {
|
|
1234
|
+
reply.images.forEach((img: ImageAttachment) => {
|
|
1235
|
+
block += `${indent} - [${img.name}] \`${img.path}\`\n`;
|
|
1236
|
+
});
|
|
1237
|
+
}
|
|
1238
|
+
block += replyBlock(reply, depth + 1);
|
|
1239
|
+
}
|
|
1240
|
+
return block;
|
|
1241
|
+
};
|
|
1242
|
+
|
|
1211
1243
|
let lastEmittedPage: string | null = null;
|
|
1212
1244
|
emitOrder.forEach((ann) => {
|
|
1213
1245
|
if (hasPageGroups && ann.pageUrl && ann.pageUrl !== lastEmittedPage) {
|
|
@@ -1268,6 +1300,12 @@ export const exportAnnotations = (
|
|
|
1268
1300
|
});
|
|
1269
1301
|
}
|
|
1270
1302
|
|
|
1303
|
+
// Threaded replies nest under the entry they answer.
|
|
1304
|
+
if (hasReplies) {
|
|
1305
|
+
const thread = replyBlock(ann);
|
|
1306
|
+
if (thread) output += `**Replies:**\n${thread}`;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1271
1309
|
output += '\n';
|
|
1272
1310
|
});
|
|
1273
1311
|
|
package/utils/quickLabels.ts
CHANGED
|
@@ -31,6 +31,19 @@ export const LABEL_COLOR_MAP: Record<string, { bg: string; text: string; darkTex
|
|
|
31
31
|
amber: { bg: 'rgba(180,83,9,0.15)', text: '#b45309', darkText: '#fbbf24' },
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The hardcoded one-click positive label behind the toolbar's π button and
|
|
36
|
+
* the composer's "Looks good" action. Deliberately NOT part of the
|
|
37
|
+
* configurable set: it is the ONLY label comment-only surfaces (HTML /
|
|
38
|
+
* live-app) may emit β their restricted handlers filter on this id.
|
|
39
|
+
*/
|
|
40
|
+
export const THUMBS_UP_LABEL: QuickLabel = {
|
|
41
|
+
id: 'thumbs-up',
|
|
42
|
+
emoji: 'π',
|
|
43
|
+
text: 'Looks good',
|
|
44
|
+
color: 'green',
|
|
45
|
+
};
|
|
46
|
+
|
|
34
47
|
export const DEFAULT_QUICK_LABELS: QuickLabel[] = [
|
|
35
48
|
{ id: 'clarify-this', emoji: 'β', text: 'Clarify this', color: 'yellow' },
|
|
36
49
|
{ id: 'missing-overview', emoji: 'πΊοΈ', text: 'Missing overview', color: 'purple', tip: 'Provide a narrative overview of what is being built, why it is being built, and how it will be built. Add this before the implementation details.' },
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* "An agent has acted" signal for the indicator policy: NOTHING visible may
|
|
3
|
+
* appear merely because `document.modelContext` exists. Only after the first
|
|
4
|
+
* successful tool call in a session does the header show its unobtrusive
|
|
5
|
+
* affordance, which subscribes here.
|
|
6
|
+
*
|
|
7
|
+
* Module-level and dependency-free; a browser without WebMCP never records a
|
|
8
|
+
* call, so subscribers render nothing and the store never changes.
|
|
9
|
+
*/
|
|
10
|
+
import { useSyncExternalStore } from 'react';
|
|
11
|
+
|
|
12
|
+
export interface WebMcpActivity {
|
|
13
|
+
/** Successful tool calls in this page load. */
|
|
14
|
+
calls: number;
|
|
15
|
+
/** Prefixed name of the last successful tool call. */
|
|
16
|
+
lastTool: string | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let activity: WebMcpActivity = { calls: 0, lastTool: null };
|
|
20
|
+
const listeners = new Set<() => void>();
|
|
21
|
+
|
|
22
|
+
export function recordToolCall(tool: string): void {
|
|
23
|
+
activity = { calls: activity.calls + 1, lastTool: tool };
|
|
24
|
+
for (const listener of listeners) listener();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getWebMcpActivity(): WebMcpActivity {
|
|
28
|
+
return activity;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function subscribeWebMcpActivity(listener: () => void): () => void {
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
return () => {
|
|
34
|
+
listeners.delete(listener);
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Mainly for tests. */
|
|
39
|
+
export function resetWebMcpActivity(): void {
|
|
40
|
+
activity = { calls: 0, lastTool: null };
|
|
41
|
+
for (const listener of listeners) listener();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function useWebMcpActivity(): WebMcpActivity {
|
|
45
|
+
return useSyncExternalStore(subscribeWebMcpActivity, getWebMcpActivity, getWebMcpActivity);
|
|
46
|
+
}
|