@plannotator/ui 0.52.0 → 0.53.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 +317 -29
- package/README.md +35 -3
- package/components/AnnotationPanel.tsx +159 -107
- package/components/AnnotationToolbar.tsx +17 -14
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/BlockRenderer.tsx +26 -14
- package/components/CodeFilePopout.tsx +131 -68
- package/components/CommentPopover.tsx +6 -349
- package/components/ComposerTokenTextarea.tsx +379 -0
- package/components/ExportModal.tsx +6 -1
- package/components/HostTitle.tsx +108 -0
- package/components/ImageLightbox.tsx +288 -17
- package/components/ImagePeek.tsx +255 -0
- package/components/ImageResolverContext.tsx +31 -0
- package/components/InlineMarkdown.tsx +123 -15
- package/components/LocalLinkContext.tsx +49 -0
- package/components/MenuVersionSection.tsx +51 -25
- package/components/OpenInAppButton.tsx +18 -49
- package/components/PlanHeaderMenu.tsx +2 -8
- package/components/ProviderIcons.tsx +10 -4
- package/components/RenderedMarkdown.tsx +40 -4
- package/components/Settings.tsx +33 -5
- package/components/SkillReferenceMenu.tsx +28 -6
- package/components/Tooltip.tsx +51 -4
- package/components/Viewer.tsx +214 -55
- package/components/ai/AIProviderBar.tsx +11 -0
- package/components/ai/AskSessionSetupHint.tsx +12 -0
- package/components/ai/DocumentAIChatPanel.tsx +17 -5
- package/components/ai/SessionAskNotice.tsx +4 -3
- package/components/blocks/BlockquoteBlock.tsx +159 -0
- package/components/blocks/HtmlBlock.tsx +121 -9
- package/components/blocks/QuestionBlock.tsx +253 -104
- package/components/diagram/DiagramViewer.tsx +41 -4
- package/components/diagram/index.ts +1 -1
- package/components/html-viewer/HtmlViewer.tsx +73 -3
- package/components/html-viewer/bridge-script.asset.js +86 -0
- package/components/html-viewer/bridge-script.ts +86 -0
- package/components/html-viewer/useHtmlAnnotation.ts +51 -3
- package/components/icons/AgentIcons.tsx +9 -7
- package/components/plan-diff/PlanCleanDiffView.tsx +239 -36
- package/components/plan-diff/PlanDiffViewer.tsx +6 -1
- package/configure.ts +27 -1
- package/hooks/useAgentJobs.ts +8 -3
- package/hooks/useAnnotationHighlighter.ts +128 -23
- package/hooks/useExternalAnnotations.ts +6 -0
- package/hooks/useOpenInApps.ts +88 -0
- package/hooks/useUpdateCheck.ts +38 -98
- package/hooks/useUpdateCheckSetting.ts +43 -0
- package/hooks/useUpdateToast.ts +101 -0
- package/package.json +2 -2
- package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
- package/shortcuts/index.ts +1 -0
- package/styles.css +1 -1
- package/types.ts +48 -0
- package/utils/agentClosed.ts +26 -0
- package/utils/agentToolSetting.ts +16 -9
- package/utils/aiProvider.ts +7 -0
- package/utils/askSessionSetupHint.ts +27 -0
- package/utils/blockTargeting.ts +14 -3
- package/utils/parser.ts +65 -51
- package/utils/pasteInsert.ts +234 -0
- package/utils/planDiffEngine.ts +272 -2
- package/utils/skillCatalog.ts +15 -2
- package/utils/skillReferences.ts +50 -13
- package/utils/storage.ts +5 -0
- package/utils/textCut.ts +41 -0
- package/utils/updateNotice.ts +120 -0
|
@@ -6,17 +6,20 @@
|
|
|
6
6
|
* hover in Viewer) — no text selection, no web-highlighter.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useEffect, useRef, useState, useCallback } from "react";
|
|
9
|
+
import React, { useEffect, useRef, useState, useCallback, createContext, useContext, useMemo } from "react";
|
|
10
10
|
import { parseMarkdownToBlocks, computeListIndices } from "../../utils/parser";
|
|
11
11
|
import { applyHighlight, codeBlockClassName, codeBlockKeepsLayout } from "../../utils/codeHighlight";
|
|
12
12
|
import { useFenceTheme } from "../../hooks/useFenceTheme";
|
|
13
13
|
import { ListItemBody } from "../ListItemBody";
|
|
14
|
+
import { BlockquoteBlock, BLOCKQUOTE_CLASS_NAME } from "../blocks/BlockquoteBlock";
|
|
14
15
|
import type { Block, Annotation, EditorMode, ImageAttachment } from "../../types";
|
|
15
16
|
import { AnnotationType } from "../../types";
|
|
16
|
-
import
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import {
|
|
18
|
+
foldUnchangedPlanDiff,
|
|
19
|
+
type PlanDiffBlock,
|
|
20
|
+
type PlanDiffFold,
|
|
21
|
+
type InlineDiffToken,
|
|
22
|
+
type InlineDiffWrap,
|
|
20
23
|
} from "../../utils/planDiffEngine";
|
|
21
24
|
import type { QuickLabel } from "../../utils/quickLabels";
|
|
22
25
|
import { AnnotationToolbar } from "../AnnotationToolbar";
|
|
@@ -24,6 +27,22 @@ import { CommentPopover } from "../CommentPopover";
|
|
|
24
27
|
import { FloatingQuickLabelPicker } from "../FloatingQuickLabelPicker";
|
|
25
28
|
import { getIdentity } from "../../utils/identity";
|
|
26
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Which version a rendered chunk comes from: 'old' for a removed block and
|
|
32
|
+
* the old half of a modified block, 'new' for everything else.
|
|
33
|
+
*/
|
|
34
|
+
export type PlanDiffSide = "old" | "new";
|
|
35
|
+
|
|
36
|
+
/** Host renderer for one parsed markdown block (see `renderBlock`). */
|
|
37
|
+
export type PlanDiffBlockRenderer = (
|
|
38
|
+
block: Block,
|
|
39
|
+
orderedIndex: number | null,
|
|
40
|
+
side: PlanDiffSide,
|
|
41
|
+
) => React.ReactNode;
|
|
42
|
+
|
|
43
|
+
/** Provided only when a host passes `renderBlock`; absent, chunks render as always. */
|
|
44
|
+
const RenderBlockContext = createContext<PlanDiffBlockRenderer | null>(null);
|
|
45
|
+
|
|
27
46
|
interface PlanCleanDiffViewProps {
|
|
28
47
|
blocks: PlanDiffBlock[];
|
|
29
48
|
annotations?: Annotation[];
|
|
@@ -38,8 +57,33 @@ interface PlanCleanDiffViewProps {
|
|
|
38
57
|
* "Classic" diff view exposed in the mode switcher.
|
|
39
58
|
*/
|
|
40
59
|
wordLevel?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Optional: render each parsed markdown block (headings, paragraphs, code,
|
|
62
|
+
* tables, …) with the host's renderer instead of the view's built-in
|
|
63
|
+
* simple one. Not used for inline word-level modified blocks. Absent, the
|
|
64
|
+
* view renders exactly as before.
|
|
65
|
+
*/
|
|
66
|
+
renderBlock?: PlanDiffBlockRenderer;
|
|
67
|
+
/**
|
|
68
|
+
* Optional: a left gutter on every block naming its source lines
|
|
69
|
+
* ("11–12", "old 15–16"; a modified block shows both). Needs the line
|
|
70
|
+
* ranges `computePlanDiff` fills. Absent, no gutter.
|
|
71
|
+
*/
|
|
72
|
+
lineGutter?: boolean;
|
|
73
|
+
/**
|
|
74
|
+
* Optional: fold long runs of unchanged sections behind a "N unchanged
|
|
75
|
+
* sections" row that expands in place (see `foldUnchangedPlanDiff`).
|
|
76
|
+
*/
|
|
77
|
+
foldUnchanged?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* Optional: classes for unchanged blocks, replacing the default dimming
|
|
80
|
+
* (`opacity-60 hover:opacity-100 transition-opacity`).
|
|
81
|
+
*/
|
|
82
|
+
unchangedClassName?: string;
|
|
41
83
|
}
|
|
42
84
|
|
|
85
|
+
const DEFAULT_UNCHANGED_CLASS = "opacity-60 hover:opacity-100 transition-opacity";
|
|
86
|
+
|
|
43
87
|
export const PlanCleanDiffView: React.FC<PlanCleanDiffViewProps> = ({
|
|
44
88
|
blocks,
|
|
45
89
|
annotations = [],
|
|
@@ -48,6 +92,10 @@ export const PlanCleanDiffView: React.FC<PlanCleanDiffViewProps> = ({
|
|
|
48
92
|
selectedAnnotationId = null,
|
|
49
93
|
mode = "selection",
|
|
50
94
|
wordLevel = true,
|
|
95
|
+
renderBlock,
|
|
96
|
+
lineGutter = false,
|
|
97
|
+
foldUnchanged = false,
|
|
98
|
+
unchangedClassName,
|
|
51
99
|
}) => {
|
|
52
100
|
const modeRef = useRef<EditorMode>(mode);
|
|
53
101
|
const onAddAnnotationRef = useRef(onAddAnnotation);
|
|
@@ -285,22 +333,104 @@ export const PlanCleanDiffView: React.FC<PlanCleanDiffViewProps> = ({
|
|
|
285
333
|
// Check if a block index has been annotated (for highlight ring)
|
|
286
334
|
const isBlockAnnotated = (index: number) => annotatedBlockIds.has(`diff-block-${index}`);
|
|
287
335
|
|
|
336
|
+
// Fold state per fold key (expanded folds render their pieces in place).
|
|
337
|
+
const [expandedFolds, setExpandedFolds] = useState<ReadonlySet<string>>(() => new Set());
|
|
338
|
+
const toggleFold = useCallback((key: string) => {
|
|
339
|
+
setExpandedFolds((prev) => {
|
|
340
|
+
const next = new Set(prev);
|
|
341
|
+
if (next.has(key)) next.delete(key);
|
|
342
|
+
else next.add(key);
|
|
343
|
+
return next;
|
|
344
|
+
});
|
|
345
|
+
}, []);
|
|
346
|
+
const segments = useMemo(
|
|
347
|
+
() => (foldUnchanged ? foldUnchangedPlanDiff(blocks) : null),
|
|
348
|
+
[blocks, foldUnchanged],
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const renderDiffBlock = (block: PlanDiffBlock, index: number, key: React.Key) => {
|
|
352
|
+
const rendered = (
|
|
353
|
+
<DiffBlockRenderer
|
|
354
|
+
key={lineGutter ? undefined : key}
|
|
355
|
+
block={block}
|
|
356
|
+
index={index}
|
|
357
|
+
hoveredIndex={hoveredBlock?.index ?? null}
|
|
358
|
+
hoveredDiffContext={hoveredBlock?.diffContext}
|
|
359
|
+
isBlockAnnotated={isBlockAnnotated}
|
|
360
|
+
wordLevel={wordLevel}
|
|
361
|
+
unchangedClassName={unchangedClassName}
|
|
362
|
+
onHover={onAddAnnotation ? (el, diffContext) => handleHover(el, block, index, diffContext) : undefined}
|
|
363
|
+
onLeave={onAddAnnotation ? handleLeave : undefined}
|
|
364
|
+
onClick={onAddAnnotation ? (el, diffContext) => handleBlockClick(block, index, el, diffContext) : undefined}
|
|
365
|
+
/>
|
|
366
|
+
);
|
|
367
|
+
if (!lineGutter) return rendered;
|
|
368
|
+
return (
|
|
369
|
+
<GutterRow key={key} labels={blockGutterLabels(block)}>
|
|
370
|
+
{rendered}
|
|
371
|
+
</GutterRow>
|
|
372
|
+
);
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
const renderFold = (fold: PlanDiffFold, index: number) => {
|
|
376
|
+
const expanded = expandedFolds.has(fold.key);
|
|
377
|
+
const noun = fold.sectionCount === 1 ? "section" : "sections";
|
|
378
|
+
const row = expanded ? (
|
|
379
|
+
<button
|
|
380
|
+
type="button"
|
|
381
|
+
data-plan-diff-fold=""
|
|
382
|
+
aria-expanded
|
|
383
|
+
onClick={() => toggleFold(fold.key)}
|
|
384
|
+
className="annotation-exclude flex items-center gap-1.5 rounded-md px-2 py-1 text-xs text-muted-foreground hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
|
385
|
+
>
|
|
386
|
+
<FoldChevron expanded />
|
|
387
|
+
Hide {fold.sectionCount} unchanged {noun}
|
|
388
|
+
</button>
|
|
389
|
+
) : (
|
|
390
|
+
<button
|
|
391
|
+
type="button"
|
|
392
|
+
data-plan-diff-fold=""
|
|
393
|
+
aria-expanded={false}
|
|
394
|
+
onClick={() => toggleFold(fold.key)}
|
|
395
|
+
className="annotation-exclude flex w-full min-w-0 items-center gap-2 rounded-md bg-muted/40 px-3 py-1.5 text-left text-xs text-muted-foreground hover:bg-muted/60 hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50"
|
|
396
|
+
>
|
|
397
|
+
<FoldChevron expanded={false} />
|
|
398
|
+
<span className="flex-none">{fold.sectionCount} unchanged {noun}</span>
|
|
399
|
+
{fold.headings.length > 0 && (
|
|
400
|
+
<span className="min-w-0 truncate text-muted-foreground/70">
|
|
401
|
+
· {fold.headings.join(" · ")}
|
|
402
|
+
</span>
|
|
403
|
+
)}
|
|
404
|
+
<span className="ml-auto flex-none pl-2">Show</span>
|
|
405
|
+
</button>
|
|
406
|
+
);
|
|
407
|
+
return (
|
|
408
|
+
<React.Fragment key={fold.key}>
|
|
409
|
+
{lineGutter ? (
|
|
410
|
+
<GutterRow labels={expanded ? [] : [rangeLabel(fold.newStart, fold.newEnd) ?? ""]}>{row}</GutterRow>
|
|
411
|
+
) : (
|
|
412
|
+
row
|
|
413
|
+
)}
|
|
414
|
+
{expanded && fold.pieces.map((piece, i) => renderDiffBlock(piece, index, `${fold.key}-${i}`))}
|
|
415
|
+
</React.Fragment>
|
|
416
|
+
);
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const body = segments
|
|
420
|
+
? segments.map((segment, i) =>
|
|
421
|
+
segment.kind === "fold"
|
|
422
|
+
? renderFold(segment, i)
|
|
423
|
+
: renderDiffBlock(segment.block, segment.index, `seg-${i}`),
|
|
424
|
+
)
|
|
425
|
+
: blocks.map((block, index) => renderDiffBlock(block, index, index));
|
|
426
|
+
|
|
288
427
|
return (
|
|
289
428
|
<div className="space-y-1">
|
|
290
|
-
{
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
hoveredIndex={hoveredBlock?.index ?? null}
|
|
296
|
-
hoveredDiffContext={hoveredBlock?.diffContext}
|
|
297
|
-
isBlockAnnotated={isBlockAnnotated}
|
|
298
|
-
wordLevel={wordLevel}
|
|
299
|
-
onHover={onAddAnnotation ? (el, diffContext) => handleHover(el, block, index, diffContext) : undefined}
|
|
300
|
-
onLeave={onAddAnnotation ? handleLeave : undefined}
|
|
301
|
-
onClick={onAddAnnotation ? (el, diffContext) => handleBlockClick(block, index, el, diffContext) : undefined}
|
|
302
|
-
/>
|
|
303
|
-
))}
|
|
429
|
+
{renderBlock ? (
|
|
430
|
+
<RenderBlockContext.Provider value={renderBlock}>{body}</RenderBlockContext.Provider>
|
|
431
|
+
) : (
|
|
432
|
+
body
|
|
433
|
+
)}
|
|
304
434
|
|
|
305
435
|
{/* Block hover toolbar (selection mode) */}
|
|
306
436
|
{hoveredBlock && !commentPopover && !quickLabelPicker && (
|
|
@@ -349,6 +479,61 @@ export const PlanCleanDiffView: React.FC<PlanCleanDiffViewProps> = ({
|
|
|
349
479
|
);
|
|
350
480
|
};
|
|
351
481
|
|
|
482
|
+
// --- Line gutter + fold row (optional seams) ---
|
|
483
|
+
|
|
484
|
+
/** "11–12", or "14" for one line; null when the side has no lines. */
|
|
485
|
+
function rangeLabel(start?: number, end?: number): string | null {
|
|
486
|
+
if (start === undefined) return null;
|
|
487
|
+
return end !== undefined && end !== start ? `${start}–${end}` : `${start}`;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/** The gutter lines for one rendered diff block. */
|
|
491
|
+
function blockGutterLabels(block: PlanDiffBlock): string[] {
|
|
492
|
+
const newLabel = rangeLabel(block.newStart, block.newEnd);
|
|
493
|
+
const oldLabel = rangeLabel(block.oldStart, block.oldEnd);
|
|
494
|
+
switch (block.type) {
|
|
495
|
+
case "removed":
|
|
496
|
+
return oldLabel ? [`old ${oldLabel}`] : [];
|
|
497
|
+
case "modified":
|
|
498
|
+
return [newLabel, oldLabel ? `old ${oldLabel}` : null].filter((l): l is string => !!l);
|
|
499
|
+
default:
|
|
500
|
+
return newLabel ? [newLabel] : [];
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** A block beside its source-line gutter. The gutter is never selectable or part of a quote. */
|
|
505
|
+
// Baseline alignment puts the label beside the first line of text, past a
|
|
506
|
+
// heading's top margin.
|
|
507
|
+
const GutterRow: React.FC<{ labels: string[]; children: React.ReactNode }> = ({ labels, children }) => (
|
|
508
|
+
<div className="flex items-baseline gap-3">
|
|
509
|
+
<div
|
|
510
|
+
data-plan-diff-gutter=""
|
|
511
|
+
aria-hidden="true"
|
|
512
|
+
className="annotation-exclude w-[4.5rem] flex-none select-none text-right font-mono text-[11px] leading-4 tabular-nums text-muted-foreground/70"
|
|
513
|
+
>
|
|
514
|
+
{labels.map((label, i) => (
|
|
515
|
+
<div key={i}>{label}</div>
|
|
516
|
+
))}
|
|
517
|
+
</div>
|
|
518
|
+
<div className="min-w-0 flex-1">{children}</div>
|
|
519
|
+
</div>
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
const FoldChevron: React.FC<{ expanded: boolean }> = ({ expanded }) => (
|
|
523
|
+
<svg
|
|
524
|
+
aria-hidden="true"
|
|
525
|
+
className={`h-3 w-3 flex-none transition-transform ${expanded ? "rotate-90" : ""}`}
|
|
526
|
+
viewBox="0 0 24 24"
|
|
527
|
+
fill="none"
|
|
528
|
+
stroke="currentColor"
|
|
529
|
+
strokeWidth={2.5}
|
|
530
|
+
strokeLinecap="round"
|
|
531
|
+
strokeLinejoin="round"
|
|
532
|
+
>
|
|
533
|
+
<path d="M9 6l6 6-6 6" />
|
|
534
|
+
</svg>
|
|
535
|
+
);
|
|
536
|
+
|
|
352
537
|
// --- DiffBlockRenderer with hover support ---
|
|
353
538
|
|
|
354
539
|
interface DiffBlockRendererProps {
|
|
@@ -359,13 +544,15 @@ interface DiffBlockRendererProps {
|
|
|
359
544
|
isBlockAnnotated: (index: number) => boolean;
|
|
360
545
|
/** When false, force block-level fallback even if inlineTokens is populated. */
|
|
361
546
|
wordLevel: boolean;
|
|
547
|
+
/** Replaces the default unchanged-block dimming classes when set. */
|
|
548
|
+
unchangedClassName?: string;
|
|
362
549
|
onHover?: (element: HTMLElement, diffContext: Annotation['diffContext']) => void;
|
|
363
550
|
onLeave?: () => void;
|
|
364
551
|
onClick?: (element: HTMLElement, diffContext: Annotation['diffContext']) => void;
|
|
365
552
|
}
|
|
366
553
|
|
|
367
554
|
const DiffBlockRenderer: React.FC<DiffBlockRendererProps> = ({
|
|
368
|
-
block, index, hoveredIndex, hoveredDiffContext, isBlockAnnotated, wordLevel, onHover, onLeave, onClick,
|
|
555
|
+
block, index, hoveredIndex, hoveredDiffContext, isBlockAnnotated, wordLevel, unchangedClassName, onHover, onLeave, onClick,
|
|
369
556
|
}) => {
|
|
370
557
|
const hoverProps = (diffContext: Annotation['diffContext']) => onHover ? {
|
|
371
558
|
onMouseEnter: (e: React.MouseEvent<HTMLElement>) => onHover(e.currentTarget, diffContext),
|
|
@@ -386,7 +573,7 @@ const DiffBlockRenderer: React.FC<DiffBlockRendererProps> = ({
|
|
|
386
573
|
switch (block.type) {
|
|
387
574
|
case "unchanged":
|
|
388
575
|
return (
|
|
389
|
-
<div className=
|
|
576
|
+
<div className={`plan-diff-unchanged ${unchangedClassName ?? DEFAULT_UNCHANGED_CLASS}`}>
|
|
390
577
|
<MarkdownChunk content={block.content} />
|
|
391
578
|
</div>
|
|
392
579
|
);
|
|
@@ -409,7 +596,7 @@ const DiffBlockRenderer: React.FC<DiffBlockRendererProps> = ({
|
|
|
409
596
|
data-diff-block-index={index}
|
|
410
597
|
{...hoverProps('removed')}
|
|
411
598
|
>
|
|
412
|
-
<MarkdownChunk content={block.content} />
|
|
599
|
+
<MarkdownChunk content={block.content} side="old" />
|
|
413
600
|
</div>
|
|
414
601
|
);
|
|
415
602
|
|
|
@@ -435,7 +622,7 @@ const DiffBlockRenderer: React.FC<DiffBlockRendererProps> = ({
|
|
|
435
622
|
className={`plan-diff-removed line-through decoration-destructive/30 opacity-60 transition-shadow ${ringClass('removed')}`}
|
|
436
623
|
{...hoverProps('removed')}
|
|
437
624
|
>
|
|
438
|
-
<MarkdownChunk content={block.oldContent!} />
|
|
625
|
+
<MarkdownChunk content={block.oldContent!} side="old" />
|
|
439
626
|
</div>
|
|
440
627
|
<div
|
|
441
628
|
className={`plan-diff-added transition-shadow ${ringClass('modified')}`}
|
|
@@ -566,7 +753,8 @@ const InlineModifiedBlock: React.FC<InlineModifiedBlockProps> = ({
|
|
|
566
753
|
|
|
567
754
|
// --- Rendering components (unchanged from main) ---
|
|
568
755
|
|
|
569
|
-
const MarkdownChunk: React.FC<{ content: string }> = ({ content }) => {
|
|
756
|
+
const MarkdownChunk: React.FC<{ content: string; side?: PlanDiffSide }> = ({ content, side = "new" }) => {
|
|
757
|
+
const renderBlock = useContext(RenderBlockContext);
|
|
570
758
|
const blocks = React.useMemo(
|
|
571
759
|
() => parseMarkdownToBlocks(content),
|
|
572
760
|
[content]
|
|
@@ -580,6 +768,16 @@ const MarkdownChunk: React.FC<{ content: string }> = ({ content }) => {
|
|
|
580
768
|
[blocks]
|
|
581
769
|
);
|
|
582
770
|
|
|
771
|
+
if (renderBlock) {
|
|
772
|
+
return (
|
|
773
|
+
<>
|
|
774
|
+
{blocks.map((block, i) => (
|
|
775
|
+
<React.Fragment key={block.id}>{renderBlock(block, orderedIndices[i], side)}</React.Fragment>
|
|
776
|
+
))}
|
|
777
|
+
</>
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
|
|
583
781
|
return (
|
|
584
782
|
<>
|
|
585
783
|
{blocks.map((block, i) => (
|
|
@@ -606,18 +804,23 @@ const SimpleBlockRenderer: React.FC<{ block: Block; orderedIndex?: number | null
|
|
|
606
804
|
}
|
|
607
805
|
|
|
608
806
|
case "blockquote": {
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
807
|
+
// The document's quote renderer, without a block id (the diff view
|
|
808
|
+
// keeps none): a prose quote renders as it always has, and a quote
|
|
809
|
+
// with a code fence, a list or a nested quote renders them inside one
|
|
810
|
+
// quote. Alerts keep the plain quote look they have always had here.
|
|
811
|
+
if (block.alertKind) {
|
|
812
|
+
const paragraphs = block.content.split(/\n\n+/);
|
|
813
|
+
return (
|
|
814
|
+
<blockquote className={BLOCKQUOTE_CLASS_NAME}>
|
|
815
|
+
{paragraphs.map((para, i) => (
|
|
816
|
+
<p key={i} className={i > 0 ? "mt-2" : ""}>
|
|
817
|
+
<InlineMarkdown text={para} />
|
|
818
|
+
</p>
|
|
819
|
+
))}
|
|
820
|
+
</blockquote>
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
return <BlockquoteBlock block={{ ...block, id: undefined as unknown as string }} />;
|
|
621
824
|
}
|
|
622
825
|
|
|
623
826
|
case "list-item": {
|
|
@@ -17,6 +17,7 @@ import { PlanCleanDiffView } from "./PlanCleanDiffView";
|
|
|
17
17
|
import { PlanRawDiffView } from "./PlanRawDiffView";
|
|
18
18
|
import { PlanDiffBadge } from "./PlanDiffBadge";
|
|
19
19
|
import { VSCodeIcon } from "./VSCodeIcon";
|
|
20
|
+
import { noteServerSessionMismatch, withServerSession } from "../../utils/serverSession";
|
|
20
21
|
|
|
21
22
|
interface PlanDiffViewerProps {
|
|
22
23
|
diffBlocks: PlanDiffBlock[];
|
|
@@ -44,8 +45,12 @@ const defaultOpenVscodeDiff = async (
|
|
|
44
45
|
const res = await fetch("/api/plan/vscode-diff", {
|
|
45
46
|
method: "POST",
|
|
46
47
|
headers: { "Content-Type": "application/json" },
|
|
47
|
-
|
|
48
|
+
// The stale-tab guard (a no-op for a host that never adopted a session).
|
|
49
|
+
body: JSON.stringify(withServerSession({ baseVersion })),
|
|
48
50
|
});
|
|
51
|
+
if (await noteServerSessionMismatch(res)) {
|
|
52
|
+
return { error: "This review was replaced. Reload the page." };
|
|
53
|
+
}
|
|
49
54
|
const data = (await res.json()) as { ok?: boolean; error?: string };
|
|
50
55
|
if (!res.ok || data.error) {
|
|
51
56
|
return { error: data.error || "Failed to open VS Code diff" };
|
package/configure.ts
CHANGED
|
@@ -7,15 +7,17 @@ import { setFileTreeBackend, type FileTreeBackend } from './hooks/useFileBrowser
|
|
|
7
7
|
import { setDraftTransport, type DraftTransport } from './hooks/useAnnotationDraft';
|
|
8
8
|
import { setExternalAnnotationTransport, type ExternalAnnotationTransport } from './hooks/useExternalAnnotations';
|
|
9
9
|
import { setAITransport, type AITransport } from './hooks/useAIChat';
|
|
10
|
-
import { setSkillCatalogTransport, setSkillContentTransport, type SkillCatalogTransport, type SkillContentTransport } from './utils/skillCatalog';
|
|
10
|
+
import { setSkillCatalogExportRegistration, setSkillCatalogTransport, setSkillContentTransport, type SkillCatalogTransport, type SkillContentTransport } from './utils/skillCatalog';
|
|
11
11
|
import { setWebMcpPolicy, type WebMcpPolicy } from './webmcp/policy';
|
|
12
12
|
import { setMathRendererLoader, type MathRenderer, type MathRendererLoader } from './utils/math';
|
|
13
13
|
import { setIdentityGenerator, type IdentityGenerator } from './utils/generateIdentity';
|
|
14
14
|
import { setAlertIconRenderer, type AlertIconRenderer } from './components/blocks/AlertBlock';
|
|
15
|
+
import { setSkillInstructionsDestination } from './components/SkillReferenceMenu';
|
|
15
16
|
import { setFenceThemeResolver, type FenceThemeResolver } from './utils/syntaxTheme';
|
|
16
17
|
import { configStore } from './config';
|
|
17
18
|
import type { ServerSyncFn } from './config/configStore';
|
|
18
19
|
import type { ExternalAnnotationEvent, VaultNode } from './types';
|
|
20
|
+
import { setComponentTooltips } from './components/HostTitle';
|
|
19
21
|
|
|
20
22
|
// One-stop type barrel: every seam contract a host implements is importable
|
|
21
23
|
// from this module, next to configurePlannotatorUI itself.
|
|
@@ -66,6 +68,19 @@ export interface PlannotatorUIConfig {
|
|
|
66
68
|
skillCatalogTransport?: SkillCatalogTransport;
|
|
67
69
|
/** Human-only skill contents request for feedback injection. Default: `GET /api/skills/content?name=` on the page origin. */
|
|
68
70
|
skillContentTransport?: SkillContentTransport;
|
|
71
|
+
/**
|
|
72
|
+
* Register the fetched skill catalog for the per-comment feedback export
|
|
73
|
+
* (each comment's "Skills referenced" list). Default: true. A host that
|
|
74
|
+
* composes one block per message itself passes false, so a comment's
|
|
75
|
+
* `$name` stays plain text in its export.
|
|
76
|
+
*/
|
|
77
|
+
skillReferenceExport?: boolean;
|
|
78
|
+
/**
|
|
79
|
+
* What a composer's "Includes skill instructions" line says a human-only
|
|
80
|
+
* skill's instructions go with. Default: "your feedback". The Plannotator
|
|
81
|
+
* Inbox, where a comment rides a reply, says "your reply".
|
|
82
|
+
*/
|
|
83
|
+
skillInstructionsDestination?: string;
|
|
69
84
|
serverSync?: ServerSyncFn;
|
|
70
85
|
/**
|
|
71
86
|
* WebMCP provider policy: `{ enabled, namePrefix }`. Default: enabled
|
|
@@ -104,6 +119,14 @@ export interface PlannotatorUIConfig {
|
|
|
104
119
|
* pane keeps its own pair. An unknown theme name renders plain text.
|
|
105
120
|
*/
|
|
106
121
|
fenceTheme?: FenceThemeResolver;
|
|
122
|
+
/**
|
|
123
|
+
* Draw ui's own hover text (the annotation panel's and toolbar's buttons, a
|
|
124
|
+
* question card's decision tag, a host-resolved local link's
|
|
125
|
+
* `LocalLinkResolution.title`) as ui's `Tooltip` instead of a native
|
|
126
|
+
* `title` (`components/HostTitle`). Default: false, native titles exactly
|
|
127
|
+
* as before. The Plannotator Inbox passes true.
|
|
128
|
+
*/
|
|
129
|
+
componentTooltips?: boolean;
|
|
107
130
|
/** Re-hydrate settings from the installed (SYNCHRONOUS) storageBackend after install. */
|
|
108
131
|
loadSettingsFromBackend?: boolean;
|
|
109
132
|
}
|
|
@@ -120,12 +143,15 @@ export function configurePlannotatorUI(config: PlannotatorUIConfig): void {
|
|
|
120
143
|
if (config.aiTransport) setAITransport(config.aiTransport);
|
|
121
144
|
if (config.skillCatalogTransport) setSkillCatalogTransport(config.skillCatalogTransport);
|
|
122
145
|
if (config.skillContentTransport) setSkillContentTransport(config.skillContentTransport);
|
|
146
|
+
if (config.skillReferenceExport !== undefined) setSkillCatalogExportRegistration(config.skillReferenceExport);
|
|
147
|
+
if (config.skillInstructionsDestination) setSkillInstructionsDestination(config.skillInstructionsDestination);
|
|
123
148
|
if (config.serverSync) configStore.setServerSync(config.serverSync);
|
|
124
149
|
if (config.webmcp) setWebMcpPolicy(config.webmcp);
|
|
125
150
|
if (config.mathRendererLoader) setMathRendererLoader(config.mathRendererLoader);
|
|
126
151
|
if (config.identityGenerator) setIdentityGenerator(config.identityGenerator);
|
|
127
152
|
if (config.alertIconRenderer) setAlertIconRenderer(config.alertIconRenderer);
|
|
128
153
|
if (config.fenceTheme) setFenceThemeResolver(config.fenceTheme);
|
|
154
|
+
if (config.componentTooltips !== undefined) setComponentTooltips(config.componentTooltips);
|
|
129
155
|
// Re-hydrate AFTER storageBackend is installed (load-bearing order — gated last).
|
|
130
156
|
if (config.loadSettingsFromBackend) configStore.loadFromBackend();
|
|
131
157
|
}
|
package/hooks/useAgentJobs.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
14
14
|
import type { AgentJobInfo, AgentJobEvent, AgentCapabilities } from '../types';
|
|
15
|
+
import { noteServerSessionMismatch, withServerSession, withServerSessionQuery } from '../utils/serverSession';
|
|
15
16
|
|
|
16
17
|
const POLL_INTERVAL_MS = 500;
|
|
17
18
|
const STREAM_URL = '/api/agents/jobs/stream';
|
|
@@ -273,9 +274,11 @@ export function useAgentJobs(
|
|
|
273
274
|
const res = await fetch(JOBS_URL, {
|
|
274
275
|
method: 'POST',
|
|
275
276
|
headers: { 'Content-Type': 'application/json' },
|
|
276
|
-
|
|
277
|
+
// The stale-tab guard (a no-op for a host that never adopted a session).
|
|
278
|
+
body: JSON.stringify(withServerSession(params)),
|
|
277
279
|
});
|
|
278
280
|
if (!res.ok) {
|
|
281
|
+
await noteServerSessionMismatch(res);
|
|
279
282
|
throw new Error(await readResponseError(res));
|
|
280
283
|
}
|
|
281
284
|
|
|
@@ -289,9 +292,10 @@ export function useAgentJobs(
|
|
|
289
292
|
|
|
290
293
|
const killJob = useCallback(async (id: string) => {
|
|
291
294
|
try {
|
|
292
|
-
await fetch(`${JOBS_URL}/${encodeURIComponent(id)}
|
|
295
|
+
const res = await fetch(withServerSessionQuery(`${JOBS_URL}/${encodeURIComponent(id)}`), {
|
|
293
296
|
method: 'DELETE',
|
|
294
297
|
});
|
|
298
|
+
await noteServerSessionMismatch(res);
|
|
295
299
|
} catch {
|
|
296
300
|
// SSE will reconcile
|
|
297
301
|
}
|
|
@@ -299,7 +303,8 @@ export function useAgentJobs(
|
|
|
299
303
|
|
|
300
304
|
const killAll = useCallback(async () => {
|
|
301
305
|
try {
|
|
302
|
-
await fetch(JOBS_URL, { method: 'DELETE' });
|
|
306
|
+
const res = await fetch(withServerSessionQuery(JOBS_URL), { method: 'DELETE' });
|
|
307
|
+
await noteServerSessionMismatch(res);
|
|
303
308
|
} catch {
|
|
304
309
|
// SSE will reconcile
|
|
305
310
|
}
|