@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
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header controls for a raw-HTML or live-app annotation surface: the eye
|
|
3
|
+
* (show/hide the floating tools over the page), the optional refresh, and
|
|
4
|
+
* the pen (Annotate/Interact toggle). Presentation only; every state lives
|
|
5
|
+
* in the host. Each control renders only when its handler is passed, so a
|
|
6
|
+
* read-only document can show the eye without a pen.
|
|
7
|
+
*
|
|
8
|
+
* The markup, data attributes (`data-html-tools-toggle`, `data-html-refresh`,
|
|
9
|
+
* `data-html-annotate-toggle`), aria state and the pixel-stable pen border
|
|
10
|
+
* are the exact ones Plannotator's header shipped with; hosts get the same
|
|
11
|
+
* control, and `labels` overrides the strings without touching the DOM.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** String overrides. Every key optional; defaults are Plannotator's strings. */
|
|
15
|
+
export interface HtmlSurfaceControlLabels {
|
|
16
|
+
/** Pen title while Annotate is armed. */
|
|
17
|
+
annotateTitle?: string;
|
|
18
|
+
/** Pen title while in Interact mode. */
|
|
19
|
+
interactTitle?: string;
|
|
20
|
+
/** Pen aria-label while armed. Default: none (the title carries the name). */
|
|
21
|
+
annotateLabel?: string;
|
|
22
|
+
/** Pen aria-label while in Interact mode. Default: none. */
|
|
23
|
+
interactLabel?: string;
|
|
24
|
+
/** Eye title and screen-reader text while the tools are visible. */
|
|
25
|
+
hideTools?: string;
|
|
26
|
+
/** Eye title and screen-reader text while the tools are hidden. */
|
|
27
|
+
showTools?: string;
|
|
28
|
+
/** Refresh visible text while idle. */
|
|
29
|
+
refresh?: string;
|
|
30
|
+
/** Refresh visible text while a refresh is in flight. */
|
|
31
|
+
refreshing?: string;
|
|
32
|
+
/** Refresh title and aria-label while idle. */
|
|
33
|
+
refreshTitle?: string;
|
|
34
|
+
/** Refresh title and aria-label while a refresh is in flight. */
|
|
35
|
+
refreshingTitle?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const DEFAULT_HTML_SURFACE_CONTROL_LABELS: Required<
|
|
39
|
+
Omit<HtmlSurfaceControlLabels, 'annotateLabel' | 'interactLabel'>
|
|
40
|
+
> = {
|
|
41
|
+
annotateTitle: 'Annotate mode: click an element or select text to comment. Esc to interact',
|
|
42
|
+
interactTitle: 'Interact mode: clicks reach the page (text selection still comments). Click to annotate',
|
|
43
|
+
hideTools: 'Hide tools',
|
|
44
|
+
showTools: 'Show tools',
|
|
45
|
+
refresh: 'Refresh',
|
|
46
|
+
refreshing: 'Refreshing',
|
|
47
|
+
refreshTitle: 'Refresh document',
|
|
48
|
+
refreshingTitle: 'Refreshing document',
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export interface HtmlSurfaceControlsProps {
|
|
52
|
+
/** Whether Annotate is armed (pen pressed). */
|
|
53
|
+
armed: boolean;
|
|
54
|
+
/** Flip Annotate/Interact. The pen renders only when provided. */
|
|
55
|
+
onToggleArmed?: () => void;
|
|
56
|
+
/** Whether the floating tools over the page are hidden (eye-off). */
|
|
57
|
+
toolsHidden?: boolean;
|
|
58
|
+
/** Flip the tools. The eye (and the refresh beside it) render only when provided. */
|
|
59
|
+
onToggleTools?: () => void;
|
|
60
|
+
/** Whether a refresh is offered for this document. */
|
|
61
|
+
canRefresh?: boolean;
|
|
62
|
+
onRefresh?: () => void;
|
|
63
|
+
isRefreshing?: boolean;
|
|
64
|
+
/** Compact touch shells put these actions in a menu instead: render nothing. */
|
|
65
|
+
compact?: boolean;
|
|
66
|
+
labels?: HtmlSurfaceControlLabels;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function HtmlSurfaceControls({
|
|
70
|
+
armed,
|
|
71
|
+
onToggleArmed,
|
|
72
|
+
toolsHidden = false,
|
|
73
|
+
onToggleTools,
|
|
74
|
+
canRefresh = false,
|
|
75
|
+
onRefresh,
|
|
76
|
+
isRefreshing = false,
|
|
77
|
+
compact = false,
|
|
78
|
+
labels,
|
|
79
|
+
}: HtmlSurfaceControlsProps) {
|
|
80
|
+
if (compact) return null;
|
|
81
|
+
const text = { ...DEFAULT_HTML_SURFACE_CONTROL_LABELS, ...labels };
|
|
82
|
+
const penLabel = armed ? labels?.annotateLabel : labels?.interactLabel;
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
{/* Show/hide tools: removes ALL floating chrome (sidebar tongue tabs +
|
|
86
|
+
the comment/attachments cluster) from the DOM, leaving nothing over
|
|
87
|
+
the page. Sits left of the pen; this button is the only way back,
|
|
88
|
+
so it never hides itself. Eye = tools visible, eye-off = hidden. */}
|
|
89
|
+
{onToggleTools && (
|
|
90
|
+
<div className="ml-1 flex items-center gap-0.5">
|
|
91
|
+
{canRefresh && onRefresh && (
|
|
92
|
+
<button
|
|
93
|
+
type="button"
|
|
94
|
+
data-html-refresh
|
|
95
|
+
// aria-disabled rather than disabled: a disabled control drops
|
|
96
|
+
// keyboard focus to body when activated. useHtmlRefresh already
|
|
97
|
+
// dedups in-flight requests, so an extra click is harmless.
|
|
98
|
+
onClick={isRefreshing ? undefined : onRefresh}
|
|
99
|
+
aria-disabled={isRefreshing}
|
|
100
|
+
className="flex items-center gap-1.5 rounded px-1.5 py-1 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground aria-disabled:cursor-wait aria-disabled:opacity-70"
|
|
101
|
+
title={isRefreshing ? text.refreshingTitle : text.refreshTitle}
|
|
102
|
+
aria-label={isRefreshing ? text.refreshingTitle : text.refreshTitle}
|
|
103
|
+
>
|
|
104
|
+
<svg
|
|
105
|
+
aria-hidden="true"
|
|
106
|
+
viewBox="0 0 24 24"
|
|
107
|
+
fill="none"
|
|
108
|
+
stroke="currentColor"
|
|
109
|
+
strokeWidth="2"
|
|
110
|
+
strokeLinecap="round"
|
|
111
|
+
strokeLinejoin="round"
|
|
112
|
+
className={`h-3.5 w-3.5 ${isRefreshing ? 'animate-spin motion-reduce:animate-none' : ''}`}
|
|
113
|
+
>
|
|
114
|
+
<path d="M20 11a8.1 8.1 0 0 0-15.5-2M4 4v5h5" />
|
|
115
|
+
<path d="M4 13a8.1 8.1 0 0 0 15.5 2M20 20v-5h-5" />
|
|
116
|
+
</svg>
|
|
117
|
+
<span className="hidden sm:inline">{isRefreshing ? text.refreshing : text.refresh}</span>
|
|
118
|
+
</button>
|
|
119
|
+
)}
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
data-html-tools-toggle
|
|
123
|
+
onClick={onToggleTools}
|
|
124
|
+
aria-pressed={toolsHidden}
|
|
125
|
+
className="cursor-pointer rounded-md border border-transparent p-1.5 text-xs font-medium text-muted-foreground transition-all hover:bg-muted hover:text-foreground"
|
|
126
|
+
title={toolsHidden ? text.showTools : text.hideTools}
|
|
127
|
+
>
|
|
128
|
+
{toolsHidden ? (
|
|
129
|
+
<svg aria-hidden="true" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
130
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M3.98 8.223A10.477 10.477 0 0 0 1.934 12C3.226 16.338 7.244 19.5 12 19.5c.993 0 1.953-.138 2.863-.395M6.228 6.228A10.45 10.45 0 0 1 12 4.5c4.756 0 8.773 3.162 10.065 7.498a10.523 10.523 0 0 1-4.293 5.774M6.228 6.228 3 3m3.228 3.228 3.65 3.65m7.894 7.894L21 21m-3.228-3.228-3.65-3.65m0 0a3 3 0 1 0-4.243-4.243m4.242 4.242L9.88 9.88" />
|
|
131
|
+
</svg>
|
|
132
|
+
) : (
|
|
133
|
+
<svg aria-hidden="true" className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
134
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
|
|
135
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
|
136
|
+
</svg>
|
|
137
|
+
)}
|
|
138
|
+
<span className="sr-only">{toolsHidden ? text.showTools : text.hideTools}</span>
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
)}
|
|
142
|
+
|
|
143
|
+
{/* Interact/Annotate toggle. A PEN icon (deliberately not a speech
|
|
144
|
+
bubble: an annotations-panel bubble beside it must stay
|
|
145
|
+
distinguishable at a glance, and so must AI sparkles). Always the
|
|
146
|
+
same icon: armed shows the accent color plus a visible border;
|
|
147
|
+
unarmed is muted with a TRANSPARENT border of the same width, so
|
|
148
|
+
the button's box is pixel-identical in both states. */}
|
|
149
|
+
{onToggleArmed && (
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
data-html-annotate-toggle
|
|
153
|
+
onClick={onToggleArmed}
|
|
154
|
+
aria-pressed={armed}
|
|
155
|
+
className={`p-1.5 rounded-md border text-xs font-medium transition-all cursor-pointer ${
|
|
156
|
+
armed
|
|
157
|
+
? 'border-primary/60 bg-primary/15 text-primary'
|
|
158
|
+
: 'border-transparent text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
159
|
+
}`}
|
|
160
|
+
title={armed ? text.annotateTitle : text.interactTitle}
|
|
161
|
+
{...(penLabel !== undefined ? { 'aria-label': penLabel } : {})}
|
|
162
|
+
>
|
|
163
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
164
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M16.862 4.487l1.687-1.688a1.875 1.875 0 112.652 2.652L6.832 19.82a4.5 4.5 0 01-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 011.13-1.897L16.862 4.487zm0 0L19.5 7.125" />
|
|
165
|
+
</svg>
|
|
166
|
+
</button>
|
|
167
|
+
)}
|
|
168
|
+
</>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -9,7 +9,8 @@ import { getImageSrc } from "./ImageThumbnail";
|
|
|
9
9
|
import { useCodePathValidation, type CodePathValidationContextValue } from "./CodePathValidationContext";
|
|
10
10
|
import type { ValidationEntry } from "../hooks/useValidatedCodePaths";
|
|
11
11
|
import { CodeFilePicker } from "./CodeFilePicker";
|
|
12
|
-
import { normalizeMathTex, renderMathToHtml } from "
|
|
12
|
+
import { normalizeMathTex, renderMathToHtml } from "../utils/math";
|
|
13
|
+
import { useMathRenderer } from "../hooks/useMathRenderer";
|
|
13
14
|
|
|
14
15
|
export interface DocPreviewResult {
|
|
15
16
|
contents?: string;
|
|
@@ -42,6 +43,9 @@ export const resetDocPreviewFetcher = (): void => {
|
|
|
42
43
|
docPreviewFetcher = defaultDocPreviewFetcher;
|
|
43
44
|
};
|
|
44
45
|
|
|
46
|
+
/** Read the active fetcher at call time (so a late override is honored). */
|
|
47
|
+
export const getDocPreviewFetcher = (): DocPreviewFetcher => docPreviewFetcher;
|
|
48
|
+
|
|
45
49
|
/**
|
|
46
50
|
* Decide how a candidate code-file path should render based on validation state:
|
|
47
51
|
* - 'link' → clickable, opens directly via onOpenCodeFile(resolvedOrInput)
|
|
@@ -309,7 +313,23 @@ const CodeFileIcon = () => (
|
|
|
309
313
|
|
|
310
314
|
const InlineMath: React.FC<{ tex: string }> = ({ tex }) => {
|
|
311
315
|
const normalizedTex = normalizeMathTex(tex);
|
|
312
|
-
const
|
|
316
|
+
const renderer = useMathRenderer();
|
|
317
|
+
const html = useMemo(() => renderMathToHtml(normalizedTex, false, renderer), [normalizedTex, renderer]);
|
|
318
|
+
|
|
319
|
+
// Same wrapper in both branches (see MathBlock): the placeholder carries the
|
|
320
|
+
// attributes annotation restore keys on, with the TeX as a text child.
|
|
321
|
+
if (html === null) {
|
|
322
|
+
return (
|
|
323
|
+
<span
|
|
324
|
+
className="math-inline math-annotatable text-foreground"
|
|
325
|
+
data-math-tex={normalizedTex}
|
|
326
|
+
data-math-display="false"
|
|
327
|
+
aria-label={normalizedTex}
|
|
328
|
+
>
|
|
329
|
+
{normalizedTex}
|
|
330
|
+
</span>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
313
333
|
|
|
314
334
|
return (
|
|
315
335
|
<span
|
|
@@ -1,33 +1,26 @@
|
|
|
1
1
|
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
-
import
|
|
3
|
+
import type { Mermaid } from 'mermaid';
|
|
4
4
|
import type { Block } from '../types';
|
|
5
5
|
import { normalizeMermaidSvgMarkup } from './mermaidSvg';
|
|
6
|
+
import {
|
|
7
|
+
MERMAID_CONFIG,
|
|
8
|
+
getMermaidRetryDelayMs,
|
|
9
|
+
loadMermaidRuntime,
|
|
10
|
+
__setMermaidRuntimeLoaderForTests,
|
|
11
|
+
} from '../utils/mermaid';
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
background: '#1e293b',
|
|
19
|
-
mainBkg: '#1e293b',
|
|
20
|
-
nodeBorder: '#475569',
|
|
21
|
-
clusterBkg: '#1e293b',
|
|
22
|
-
clusterBorder: '#475569',
|
|
23
|
-
titleColor: '#f8fafc',
|
|
24
|
-
edgeLabelBackground: '#1e293b',
|
|
25
|
-
},
|
|
26
|
-
flowchart: {
|
|
27
|
-
htmlLabels: true,
|
|
28
|
-
curve: 'basis',
|
|
29
|
-
},
|
|
30
|
-
});
|
|
13
|
+
// Re-exported: the config pin test and the lazy-retry test import them from here.
|
|
14
|
+
export { MERMAID_CONFIG, __setMermaidRuntimeLoaderForTests };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The runtime comes from the slot in utils/mermaid: filled eagerly by
|
|
18
|
+
* Plannotator (utils/mermaid-eager, imported by the editor App), loaded
|
|
19
|
+
* lazily otherwise. See that module for the retry contract.
|
|
20
|
+
*/
|
|
21
|
+
const getMermaid = loadMermaidRuntime;
|
|
22
|
+
|
|
23
|
+
const wait = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
31
24
|
|
|
32
25
|
interface ViewBox {
|
|
33
26
|
x: number;
|
|
@@ -133,6 +126,11 @@ const MermaidBlockImpl: React.FC<{ block: Block }> = ({ block }) => {
|
|
|
133
126
|
const expandedOverlayRef = useRef<HTMLDivElement>(null);
|
|
134
127
|
const [svg, setSvg] = useState('');
|
|
135
128
|
const [error, setError] = useState<string | null>(null);
|
|
129
|
+
// True when the failure was the runtime import itself (a chunking host's
|
|
130
|
+
// fetch), which is the only failure a Retry can change; a diagram syntax
|
|
131
|
+
// error keeps the panel exactly as it always was.
|
|
132
|
+
const [runtimeUnavailable, setRuntimeUnavailable] = useState(false);
|
|
133
|
+
const [retryToken, setRetryToken] = useState(0);
|
|
136
134
|
const [showSource, setShowSource] = useState(false);
|
|
137
135
|
const [isExpanded, setIsExpanded] = useState(false);
|
|
138
136
|
|
|
@@ -188,6 +186,27 @@ const MermaidBlockImpl: React.FC<{ block: Block }> = ({ block }) => {
|
|
|
188
186
|
|
|
189
187
|
// Render mermaid diagram
|
|
190
188
|
const renderDiagram = async () => {
|
|
189
|
+
let mermaid: Mermaid;
|
|
190
|
+
try {
|
|
191
|
+
try {
|
|
192
|
+
mermaid = await getMermaid();
|
|
193
|
+
} catch {
|
|
194
|
+
// Transient chunk failure on a chunking host: one automatic
|
|
195
|
+
// re-attempt with a fresh import() after a short delay. In a
|
|
196
|
+
// single-file build the first await never rejects, so this branch
|
|
197
|
+
// is unreachable there and the success path is unchanged.
|
|
198
|
+
await wait(getMermaidRetryDelayMs());
|
|
199
|
+
if (cancelled) return;
|
|
200
|
+
mermaid = await getMermaid();
|
|
201
|
+
}
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (!cancelled) {
|
|
204
|
+
setError(err instanceof Error ? err.message : 'Failed to render diagram');
|
|
205
|
+
setRuntimeUnavailable(true);
|
|
206
|
+
setSvg('');
|
|
207
|
+
}
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
191
210
|
try {
|
|
192
211
|
const id = `mermaid-${block.id}`;
|
|
193
212
|
const { svg: renderedSvg } = await mermaid.render(id, block.content);
|
|
@@ -196,10 +215,12 @@ const MermaidBlockImpl: React.FC<{ block: Block }> = ({ block }) => {
|
|
|
196
215
|
naturalBoundsRef.current = parseViewBoxFromMarkup(normalizedSvg);
|
|
197
216
|
setSvg(normalizedSvg);
|
|
198
217
|
setError(null);
|
|
218
|
+
setRuntimeUnavailable(false);
|
|
199
219
|
}
|
|
200
220
|
} catch (err) {
|
|
201
221
|
if (!cancelled) {
|
|
202
222
|
setError(err instanceof Error ? err.message : 'Failed to render diagram');
|
|
223
|
+
setRuntimeUnavailable(false);
|
|
203
224
|
setSvg('');
|
|
204
225
|
}
|
|
205
226
|
}
|
|
@@ -210,7 +231,7 @@ const MermaidBlockImpl: React.FC<{ block: Block }> = ({ block }) => {
|
|
|
210
231
|
return () => {
|
|
211
232
|
cancelled = true;
|
|
212
233
|
};
|
|
213
|
-
}, [block.content, block.id]);
|
|
234
|
+
}, [block.content, block.id, retryToken]);
|
|
214
235
|
|
|
215
236
|
// Reset zoom and pan when content changes
|
|
216
237
|
useEffect(() => {
|
|
@@ -419,6 +440,19 @@ const MermaidBlockImpl: React.FC<{ block: Block }> = ({ block }) => {
|
|
|
419
440
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
|
420
441
|
</svg>
|
|
421
442
|
<span className="text-xs text-destructive font-medium">Mermaid Error</span>
|
|
443
|
+
{runtimeUnavailable && (
|
|
444
|
+
<button
|
|
445
|
+
type="button"
|
|
446
|
+
onClick={() => {
|
|
447
|
+
setError(null);
|
|
448
|
+
setRetryToken((token) => token + 1);
|
|
449
|
+
}}
|
|
450
|
+
className="ml-auto rounded-md border border-destructive/30 px-2 py-0.5 text-xs text-destructive hover:bg-destructive/10"
|
|
451
|
+
title="Retry loading the diagram renderer"
|
|
452
|
+
>
|
|
453
|
+
Retry
|
|
454
|
+
</button>
|
|
455
|
+
)}
|
|
422
456
|
</div>
|
|
423
457
|
<pre className="p-3 text-xs text-destructive/80 overflow-x-auto">{error}</pre>
|
|
424
458
|
<pre className="p-3 text-xs text-muted-foreground bg-muted/30 border-t border-border/30 overflow-x-auto">
|
package/components/Settings.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { AnnotateAgentTerminalSide } from '@plannotator/core/agent-terminal
|
|
|
4
4
|
import type { Origin } from '@plannotator/core/agents';
|
|
5
5
|
import type { DiffLineBgIntensity } from '@plannotator/core/config-types';
|
|
6
6
|
import { configStore, useConfigValue, setReviewPanelView, setReviewDefaultDiffType } from '../config';
|
|
7
|
+
import { setWebMcpToolsEnabled, useWebMcpToolsEnabled } from '../webmcp/preference';
|
|
7
8
|
import { loadDiffFont } from '../utils/diffFonts';
|
|
8
9
|
import { TaterSpritePullup } from './TaterSpritePullup';
|
|
9
10
|
import { getIdentity, regenerateIdentity, setCustomIdentity, isIdentityEditable } from '../utils/identity';
|
|
@@ -107,6 +108,10 @@ interface SettingsProps {
|
|
|
107
108
|
* from a Hidden position, but offering it where no terminal can ever run
|
|
108
109
|
* would just be a dead control. */
|
|
109
110
|
agentTerminalAvailable?: boolean;
|
|
111
|
+
/** The browser exposes WebMCP (`document.modelContext`), so the "Agent
|
|
112
|
+
* tools" opt-out is worth showing. Default false: a browser without the
|
|
113
|
+
* API gets no row at all (the provider registers nothing there). */
|
|
114
|
+
webmcpAvailable?: boolean;
|
|
110
115
|
}
|
|
111
116
|
|
|
112
117
|
// --- Review-mode Display tab (diff display options) ---
|
|
@@ -848,7 +853,8 @@ const CommentsTab: React.FC = () => {
|
|
|
848
853
|
);
|
|
849
854
|
};
|
|
850
855
|
|
|
851
|
-
export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange, onIdentityChange, origin, mode = 'plan', onUIPreferencesChange, externalOpen, onExternalClose, aiProviders = [], gitUser, sinceBaseUnavailable, isCompactTouchLayout = false, onDetectObsidianVaults, agentTerminalAvailable = false }) => {
|
|
856
|
+
export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange, onIdentityChange, origin, mode = 'plan', onUIPreferencesChange, externalOpen, onExternalClose, aiProviders = [], gitUser, sinceBaseUnavailable, isCompactTouchLayout = false, onDetectObsidianVaults, agentTerminalAvailable = false, webmcpAvailable = false }) => {
|
|
857
|
+
const webmcpTools = useWebMcpToolsEnabled();
|
|
852
858
|
const [showDialog, setShowDialog] = useState(false);
|
|
853
859
|
const settingsWasOpenRef = useRef(false);
|
|
854
860
|
const [themePreview, setThemePreview] = useState(false);
|
|
@@ -1282,6 +1288,39 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1282
1288
|
</div>
|
|
1283
1289
|
</div>
|
|
1284
1290
|
|
|
1291
|
+
{/* Agent tools (WebMCP). Shown only when the browser
|
|
1292
|
+
exposes document.modelContext; off unregisters the
|
|
1293
|
+
tools for this browser. Cookie-only and idle at its
|
|
1294
|
+
default: no cookie exists until the user opts out. */}
|
|
1295
|
+
{webmcpAvailable && (
|
|
1296
|
+
<>
|
|
1297
|
+
<div className="border-t border-border" />
|
|
1298
|
+
<div className="flex items-center justify-between" data-webmcp-setting="true">
|
|
1299
|
+
<div>
|
|
1300
|
+
<div className="text-sm font-medium">Agent tools</div>
|
|
1301
|
+
<div className="text-xs text-muted-foreground">
|
|
1302
|
+
Let your browser's agent read this document and leave comments. Approving and sending feedback stay yours.
|
|
1303
|
+
</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
<button
|
|
1306
|
+
role="switch"
|
|
1307
|
+
aria-checked={webmcpTools}
|
|
1308
|
+
aria-label="Agent tools"
|
|
1309
|
+
onClick={() => setWebMcpToolsEnabled(!webmcpTools)}
|
|
1310
|
+
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
1311
|
+
webmcpTools ? 'bg-primary' : 'bg-muted'
|
|
1312
|
+
}`}
|
|
1313
|
+
>
|
|
1314
|
+
<span
|
|
1315
|
+
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
|
|
1316
|
+
webmcpTools ? 'translate-x-6' : 'translate-x-1'
|
|
1317
|
+
}`}
|
|
1318
|
+
/>
|
|
1319
|
+
</button>
|
|
1320
|
+
</div>
|
|
1321
|
+
</>
|
|
1322
|
+
)}
|
|
1323
|
+
|
|
1285
1324
|
{/* Permission Mode (Claude Code only) */}
|
|
1286
1325
|
{origin === 'claude-code' && mode === 'plan' && (
|
|
1287
1326
|
<>
|
|
@@ -1,26 +1,38 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
-
import katex from 'katex';
|
|
3
2
|
import type { Block } from '../../types';
|
|
3
|
+
import { normalizeMathTex, renderMathToHtml } from '../../utils/math';
|
|
4
|
+
import { useMathRenderer } from '../../hooks/useMathRenderer';
|
|
5
|
+
|
|
6
|
+
// Kept as re-exports: InlineMarkdown and older consumers import them from here.
|
|
7
|
+
export { normalizeMathTex, renderMathToHtml };
|
|
4
8
|
|
|
5
9
|
type MathBlockProps = {
|
|
6
10
|
block: Block;
|
|
7
11
|
};
|
|
8
12
|
|
|
9
|
-
export const normalizeMathTex = (tex: string): string => tex.trim();
|
|
10
|
-
|
|
11
|
-
export const renderMathToHtml = (tex: string, displayMode: boolean): string => (
|
|
12
|
-
katex.renderToString(tex, {
|
|
13
|
-
displayMode,
|
|
14
|
-
throwOnError: false,
|
|
15
|
-
strict: 'warn',
|
|
16
|
-
trust: false,
|
|
17
|
-
output: 'html',
|
|
18
|
-
})
|
|
19
|
-
);
|
|
20
|
-
|
|
21
13
|
export const MathBlock: React.FC<MathBlockProps> = ({ block }) => {
|
|
22
14
|
const tex = normalizeMathTex(block.content);
|
|
23
|
-
const
|
|
15
|
+
const renderer = useMathRenderer();
|
|
16
|
+
const html = useMemo(() => renderMathToHtml(tex, true, renderer), [tex, renderer]);
|
|
17
|
+
|
|
18
|
+
// The wrapper (class names, data-* anchors, aria-label) is identical in both
|
|
19
|
+
// branches: block targeting and math-annotation restore key on these
|
|
20
|
+
// attributes, so a placeholder must be addressable exactly like the typeset
|
|
21
|
+
// block. The placeholder's child is a React text node, never markup.
|
|
22
|
+
if (html === null) {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
className="math-block math-annotatable my-5 overflow-x-auto py-2 text-foreground"
|
|
26
|
+
data-block-id={block.id}
|
|
27
|
+
data-block-type="math"
|
|
28
|
+
data-math-tex={tex}
|
|
29
|
+
data-math-display="true"
|
|
30
|
+
aria-label={tex}
|
|
31
|
+
>
|
|
32
|
+
{tex}
|
|
33
|
+
</div>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
24
36
|
|
|
25
37
|
return (
|
|
26
38
|
<div
|