@plannotator/ui 0.52.1 → 0.53.1

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.
Files changed (76) hide show
  1. package/HANDOFF.md +284 -29
  2. package/README.md +45 -5
  3. package/components/AnnotationPanel.tsx +195 -118
  4. package/components/AnnotationToolbar.tsx +17 -14
  5. package/components/AnnotationToolstrip.tsx +54 -40
  6. package/components/BlockRenderer.tsx +20 -14
  7. package/components/CodeFilePopout.tsx +132 -69
  8. package/components/CommentPopover.tsx +8 -349
  9. package/components/ComposerTokenTextarea.tsx +379 -0
  10. package/components/ExportModal.tsx +6 -1
  11. package/components/HostTitle.tsx +108 -0
  12. package/components/HtmlSurfaceControls.tsx +2 -1
  13. package/components/ImageLightbox.tsx +288 -17
  14. package/components/ImagePeek.tsx +255 -0
  15. package/components/ImageResolverContext.tsx +31 -0
  16. package/components/InlineMarkdown.tsx +123 -15
  17. package/components/LocalLinkContext.tsx +49 -0
  18. package/components/MentionPicker.tsx +51 -0
  19. package/components/MenuVersionSection.tsx +51 -25
  20. package/components/OpenInAppButton.tsx +18 -49
  21. package/components/PlanHeaderMenu.tsx +2 -8
  22. package/components/ProviderIcons.tsx +10 -4
  23. package/components/RenderedMarkdown.tsx +40 -4
  24. package/components/Settings.tsx +33 -5
  25. package/components/SkillReferenceMenu.tsx +67 -18
  26. package/components/Tooltip.tsx +51 -4
  27. package/components/Viewer.tsx +220 -49
  28. package/components/ai/AIProviderBar.tsx +11 -0
  29. package/components/ai/AskSessionSetupHint.tsx +12 -0
  30. package/components/ai/DocumentAIChatPanel.tsx +17 -5
  31. package/components/ai/SessionAskNotice.tsx +4 -3
  32. package/components/blocks/BlockquoteBlock.tsx +159 -0
  33. package/components/blocks/HtmlBlock.tsx +121 -9
  34. package/components/blocks/QuestionBlock.tsx +183 -96
  35. package/components/blocks/QuestionContext.tsx +3 -0
  36. package/components/diagram/DiagramViewer.tsx +41 -4
  37. package/components/diagram/index.ts +1 -1
  38. package/components/html-viewer/HtmlViewer.tsx +95 -3
  39. package/components/html-viewer/bridge-script.asset.js +413 -7
  40. package/components/html-viewer/bridge-script.lite.ts +1 -1
  41. package/components/html-viewer/bridge-script.ts +454 -7
  42. package/components/html-viewer/useHtmlAnnotation.ts +51 -3
  43. package/components/icons/AgentIcons.tsx +9 -7
  44. package/components/plan-diff/PlanCleanDiffView.tsx +239 -36
  45. package/components/plan-diff/PlanDiffViewer.tsx +6 -1
  46. package/configure.ts +27 -1
  47. package/hooks/useAgentJobs.ts +8 -3
  48. package/hooks/useAnnotationHighlighter.ts +128 -23
  49. package/hooks/useExternalAnnotations.ts +6 -0
  50. package/hooks/useMentionAutocomplete.ts +1 -1
  51. package/hooks/useOpenInApps.ts +88 -0
  52. package/hooks/useSkillReferenceAutocomplete.ts +7 -3
  53. package/hooks/useUpdateCheck.ts +38 -98
  54. package/hooks/useUpdateCheckSetting.ts +43 -0
  55. package/hooks/useUpdateToast.ts +101 -0
  56. package/hooks/useVimSelection.ts +9 -0
  57. package/package.json +2 -2
  58. package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
  59. package/shortcuts/index.ts +1 -0
  60. package/shortcuts/runtime.ts +10 -3
  61. package/styles.css +1 -1
  62. package/types.ts +48 -0
  63. package/utils/agentClosed.ts +26 -0
  64. package/utils/agentToolSetting.ts +16 -9
  65. package/utils/aiProvider.ts +7 -0
  66. package/utils/askSessionSetupHint.ts +27 -0
  67. package/utils/blockTargeting.ts +14 -3
  68. package/utils/mentions.ts +34 -5
  69. package/utils/parser.ts +65 -51
  70. package/utils/pasteInsert.ts +234 -0
  71. package/utils/planDiffEngine.ts +272 -2
  72. package/utils/skillCatalog.ts +15 -2
  73. package/utils/skillReferences.ts +50 -13
  74. package/utils/storage.ts +5 -0
  75. package/utils/textCut.ts +41 -0
  76. package/utils/updateNotice.ts +120 -0
@@ -3,6 +3,7 @@ import {
3
3
  useCallback,
4
4
  useEffect,
5
5
  useImperativeHandle,
6
+ useLayoutEffect,
6
7
  useMemo,
7
8
  useRef,
8
9
  useState,
@@ -14,7 +15,7 @@ import {
14
15
  isVimSelectionActionId,
15
16
  type VimSelectionHudContext,
16
17
  } from "../../shortcuts";
17
- import type { Annotation, EditorMode, ImageAttachment, InputMethod } from "../../types";
18
+ import type { Annotation, EditorMode, HostDraft, ImageAttachment, InputMethod } from "../../types";
18
19
  import { AnnotationType } from "../../types";
19
20
  import { copyTextPreservingFocus } from "../../utils/clipboard";
20
21
  import { elementIdentityForAskAI } from "../../utils/parser";
@@ -181,6 +182,9 @@ function parseVimBridgeCopy(value: unknown): string | null {
181
182
  }
182
183
 
183
184
  /** Inputs for the sandboxed raw-HTML viewer and its parent-side annotation UI. */
185
+ /** The fewest milliseconds between two honored `step` messages from the page. */
186
+ export const STEP_MESSAGE_MIN_INTERVAL_MS = 150;
187
+
184
188
  export interface HtmlViewerProps {
185
189
  rawHtml: string;
186
190
  /** Live proxied-app mode: render `src` (no sandbox, no srcdoc) instead of
@@ -228,6 +232,20 @@ export interface HtmlViewerProps {
228
232
  /** Mod+Shift+X pressed while focus lived inside the iframe: show/hide the
229
233
  * host's floating tools over the page. The host owns that state. */
230
234
  onToolsToggle?: () => void;
235
+ /** A plain `]` (1) or `[` (-1) pressed inside the page while the surface is
236
+ * armed (Annotate) and the page's focus is not in a text field: the host's
237
+ * next / previous file (the Inbox steps through a thread's attachments).
238
+ * The bridge forwards nothing in Interact, and the page still gets the
239
+ * key. Honored only while the frame holds focus, and at most once per
240
+ * STEP_MESSAGE_MIN_INTERVAL_MS. Absent: the message is ignored, as before. */
241
+ onStep?: (dir: 1 | -1) => void;
242
+ /** Escape pressed inside the page in Interact that the page did not use
243
+ * itself (no handler of the page cancelled it or stopped it, and focus was
244
+ * not in one of its text fields): the host may close something of its own
245
+ * (the Inbox leaves full screen). Never sent in Annotate, whose Escape is
246
+ * `onAnnotateModeExit`. Honored only while the frame holds focus, and at
247
+ * most once per STEP_MESSAGE_MIN_INTERVAL_MS. Absent: ignored, as before. */
248
+ onPageEscape?: () => void;
231
249
  /** Opt-in Vim-style keyboard selection. Default false for compatibility. */
232
250
  vimModeEnabled?: boolean;
233
251
  /** Replace the iframe-local compact badge with the shared live key HUD. */
@@ -325,6 +343,27 @@ export interface HtmlViewerProps {
325
343
  * warning). Meaningless on the inline path, which never shows a strip.
326
344
  */
327
345
  bridgeErrorDisplay?: "banner" | "none";
346
+ /**
347
+ * Opt-in host capability: the composer handed to the host (see
348
+ * `HostDraft`). A text selection reports `intent: 'selection'` and a pin
349
+ * `intent: 'compose'` with the element's label, instead of opening the
350
+ * toolbar or the composer; the selection clearing reports null. With it the
351
+ * handle's `stepPin` moves a pinned draft to the element around it or back
352
+ * in. The host saves the comment and draws it through `annotations` /
353
+ * `applySharedAnnotations`. Ignored when `readOnly`. Absent → unchanged.
354
+ */
355
+ onHostDraft?: (draft: HostDraft | null) => void;
356
+ /**
357
+ * Opt-in host capability for a native shell whose web view decides every
358
+ * navigation itself (the Inbox surface in the iPhone app). A link the
359
+ * person taps in Interact opens as a new window (`window.open`, the sandbox
360
+ * gains `allow-popups`) instead of being reported through the frame
361
+ * protocol (`onOpenLink` does not fire for it); the host's web view hands
362
+ * that window to its own delegate, which decides it. A scripted click opens
363
+ * nothing. In-page `#` links still scroll here, and an armed pinpoint tap
364
+ * still pins. Absent → unchanged.
365
+ */
366
+ hostNavigates?: boolean;
328
367
  }
329
368
 
330
369
  /**
@@ -351,6 +390,8 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
351
390
  onAnnotateModeExit,
352
391
  onAnnotateModeToggle,
353
392
  onToolsToggle,
393
+ onStep,
394
+ onPageEscape,
354
395
  vimModeEnabled = false,
355
396
  vimHudEnabled = false,
356
397
  vimHudKeyPanelEnabled = true,
@@ -377,6 +418,8 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
377
418
  bridgeReadyTimeoutMs = DEFAULT_BRIDGE_READY_TIMEOUT_MS,
378
419
  onBridgeUnavailable,
379
420
  bridgeErrorDisplay = "banner",
421
+ onHostDraft,
422
+ hostNavigates = false,
380
423
  },
381
424
  ref,
382
425
  ) => {
@@ -411,6 +454,15 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
411
454
  onAnnotateModeToggleRef.current = onAnnotateModeToggle;
412
455
  const onToolsToggleRef = useRef(onToolsToggle);
413
456
  onToolsToggleRef.current = onToolsToggle;
457
+ const onStepRef = useRef(onStep);
458
+ onStepRef.current = onStep;
459
+ // The step message is honored only while the host has the surface armed.
460
+ const annotateModeActiveRef = useRef(annotateModeActive);
461
+ annotateModeActiveRef.current = annotateModeActive;
462
+ const lastStepAtRef = useRef(0);
463
+ const onPageEscapeRef = useRef(onPageEscape);
464
+ onPageEscapeRef.current = onPageEscape;
465
+ const lastPageEscapeAtRef = useRef(0);
414
466
 
415
467
  /** Single choke point for direct-to-bridge posts: live sessions get the
416
468
  * token + concrete targetOrigin, srcdoc keeps "*" and no token. */
@@ -613,6 +665,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
613
665
  onUnanchoredChange: handleBridgeUnanchored,
614
666
  maxAdditionalTargets,
615
667
  scrollBehavior,
668
+ onHostDraft: readOnly ? undefined : onHostDraft,
616
669
  });
617
670
  createdIdsRef.current = hook.createdAnnotationIds;
618
671
 
@@ -682,7 +735,12 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
682
735
  [hook.draftTargets],
683
736
  );
684
737
 
685
- useEffect(() => {
738
+ // Registered in the commit that inserts the iframe (a layout effect), so
739
+ // the bridge's `ready` can never arrive before anyone listens: a passive
740
+ // effect can run after a fast srcdoc frame has already posted it, and a
741
+ // lost ready leaves the bridge unconfigured for good (seen on a Linux
742
+ // WebKit runner, the Inbox surface's proof).
743
+ useLayoutEffect(() => {
686
744
  function handler(e: MessageEvent<unknown>) {
687
745
  if (e.source !== iframeRef.current?.contentWindow) return;
688
746
  // Live sessions verify origin + token before reading anything.
@@ -748,6 +806,31 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
748
806
  onToolsToggleRef.current?.();
749
807
  return;
750
808
  }
809
+ if (isRecord(e.data) && e.data.type === `${PREFIX}step`) {
810
+ // The page itself can post this message, so it is honored only for
811
+ // a key the person pressed: the frame holds focus, the surface is
812
+ // armed, and at most one step per STEP_MESSAGE_MIN_INTERVAL_MS (a
813
+ // page cannot push itself out of view by posting in a loop).
814
+ const dir = e.data.dir;
815
+ if (!annotateModeActiveRef.current || (dir !== 1 && dir !== -1)) return;
816
+ if (document.activeElement !== iframeRef.current) return;
817
+ const now = Date.now();
818
+ if (now - lastStepAtRef.current < STEP_MESSAGE_MIN_INTERVAL_MS) return;
819
+ lastStepAtRef.current = now;
820
+ onStepRef.current?.(dir);
821
+ return;
822
+ }
823
+ if (isRecord(e.data) && e.data.type === `${PREFIX}page-escape`) {
824
+ // Like a step, the page could post this itself: honored only for a
825
+ // key pressed in the frame, in Interact, at most once per interval.
826
+ if (annotateModeActiveRef.current) return;
827
+ if (document.activeElement !== iframeRef.current) return;
828
+ const now = Date.now();
829
+ if (now - lastPageEscapeAtRef.current < STEP_MESSAGE_MIN_INTERVAL_MS) return;
830
+ lastPageEscapeAtRef.current = now;
831
+ onPageEscapeRef.current?.();
832
+ return;
833
+ }
751
834
  const vimCopy = parseVimBridgeCopy(e.data);
752
835
  if (vimCopy !== null) {
753
836
  const iframe = iframeRef.current;
@@ -916,6 +999,12 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
916
999
  );
917
1000
  }, [iframeReadyVersion, annotateModeActive]);
918
1001
 
1002
+ // A native host decides link navigations itself (`hostNavigates`).
1003
+ useEffect(() => {
1004
+ if (iframeReadyVersion === 0 || !hostNavigates) return;
1005
+ postToBridge({ type: `${PREFIX}set-host-navigates`, active: true });
1006
+ }, [iframeReadyVersion, hostNavigates]);
1007
+
919
1008
  // Parent-side Esc rung: with focus outside the iframe the bridge never
920
1009
  // sees the keydown. Any open composer/toolbar/picker still closes first —
921
1010
  // their state is read from this render's closure, so an Esc that closed
@@ -1012,6 +1101,9 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
1012
1101
  // Shared/draft restores respect the live page filter too.
1013
1102
  applySharedAnnotations: (anns: Annotation[]) =>
1014
1103
  hook.applyAnnotations(forCurrentPage(anns)),
1104
+ stepPin: (direction: "parent" | "child") =>
1105
+ postToBridge({ type: `${PREFIX}step-pin`, direction }),
1106
+ revealPin: () => postToBridge({ type: `${PREFIX}reveal-pin` }),
1015
1107
  }));
1016
1108
 
1017
1109
  const handleGlobalCommentSubmit = useCallback(
@@ -1166,7 +1258,7 @@ export const HtmlViewer = forwardRef<ViewerHandle, HtmlViewerProps>(
1166
1258
  same-origin XHR) and no srcdoc. Srcdoc mode is unchanged. */}
1167
1259
  <iframe
1168
1260
  ref={iframeRef}
1169
- {...(src ? { src } : { srcDoc: srcdoc, sandbox: "allow-scripts" })}
1261
+ {...(src ? { src } : { srcDoc: srcdoc, sandbox: "allow-scripts" + (hostNavigates ? " allow-popups" : "") })}
1170
1262
  style={{
1171
1263
  width: "100%",
1172
1264
  height: fullViewport ? "100%" : `${iframeHeight}px`,