@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
@@ -0,0 +1,101 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { toast } from 'sonner';
3
+ import { copyTextToClipboard } from '../utils/clipboard';
4
+ import { claimUpdateToast, describeUpdateToast, requestUpdateNow, type UpdateAdvert } from '../utils/updateNotice';
5
+
6
+ /** First look after the page settles: past the first-run announcements' own 4 s window. */
7
+ export const UPDATE_TOAST_DELAY_MS = 5_000;
8
+ /** While something blocks it (a first-run dialog, a focused composer), look again this often. */
9
+ const RETRY_MS = 5_000;
10
+ /** Give up for this load after this long; the next load (or the menu) says it. */
11
+ const GIVE_UP_MS = 120_000;
12
+
13
+ function isTypingSomewhere(): boolean {
14
+ const el = typeof document !== 'undefined' ? document.activeElement : null;
15
+ if (!el || el === document.body) return false;
16
+ const tag = el.tagName;
17
+ if (tag === 'TEXTAREA' || tag === 'INPUT' || tag === 'SELECT' || (el as HTMLElement).isContentEditable) return true;
18
+ // An open dialog owns the screen; the toast waits for it.
19
+ return !!el.closest('[role="dialog"], dialog[open]');
20
+ }
21
+
22
+ /**
23
+ * The review windows' "a new version is out" toast (design §4.5): from the
24
+ * server's advert only, at most once per version and once every three days
25
+ * (claimUpdateToast), queued behind the first-run dialogs (`blocked`) and
26
+ * never while a composer or dialog has focus. A blocked attempt is not
27
+ * consumed: it looks again, and gives up for this load after two minutes.
28
+ *
29
+ * Actions: What's new (the release on GitHub) and, for the binary the install
30
+ * script manages, Update (POST /api/update runs it now); elsewhere Copy
31
+ * command (the right command for the host).
32
+ */
33
+ export function useUpdateToast(input: {
34
+ advert: UpdateAdvert | null;
35
+ /** A first-run dialog is showing or may still show this load. */
36
+ blocked: boolean;
37
+ /** Auto-update will install it; the post-update toast speaks instead. */
38
+ autoUpdateActive: boolean;
39
+ /** A share link or archive: nobody to tell. */
40
+ suppressed?: boolean;
41
+ position?: 'top-right' | 'bottom-right';
42
+ }): void {
43
+ const blockedRef = useRef(input.blocked);
44
+ blockedRef.current = input.blocked;
45
+ const shown = useRef(false);
46
+ const { advert, autoUpdateActive, suppressed, position } = input;
47
+
48
+ useEffect(() => {
49
+ if (!advert || autoUpdateActive || suppressed || shown.current) return;
50
+ const started = Date.now();
51
+ let timer: ReturnType<typeof setTimeout>;
52
+ const attempt = () => {
53
+ if (blockedRef.current || isTypingSomewhere()) {
54
+ if (Date.now() - started < GIVE_UP_MS) timer = setTimeout(attempt, RETRY_MS);
55
+ return;
56
+ }
57
+ if (!claimUpdateToast(advert.version, Date.now())) return;
58
+ shown.current = true;
59
+ const { title, description, primary } = describeUpdateToast(advert);
60
+ toast(title, {
61
+ description,
62
+ duration: 12_000,
63
+ ...(position && { position }),
64
+ closeButton: true,
65
+ cancel: {
66
+ label: "What's new",
67
+ onClick: () => window.open(advert.releaseUrl, '_blank', 'noopener,noreferrer'),
68
+ },
69
+ action:
70
+ primary === 'update'
71
+ ? {
72
+ label: 'Update',
73
+ onClick: () => {
74
+ void requestUpdateNow().then((result) => {
75
+ if (!("error" in result)) {
76
+ toast(`Updating to ${result.version}…`, {
77
+ description: 'The install script runs in the background. This review keeps working.',
78
+ duration: 8_000,
79
+ ...(position && { position }),
80
+ });
81
+ } else {
82
+ toast("The update didn't start", { description: result.error, duration: 10_000, ...(position && { position }) });
83
+ }
84
+ });
85
+ },
86
+ }
87
+ : {
88
+ label: 'Copy command',
89
+ onClick: () => {
90
+ void copyTextToClipboard(advert.command).then((copied) => {
91
+ if (copied) toast('Command copied', { description: advert.command, duration: 4_000, ...(position && { position }) });
92
+ });
93
+ },
94
+ },
95
+ classNames: { toast: '!w-auto', description: '!text-foreground/70 select-text' },
96
+ });
97
+ };
98
+ timer = setTimeout(attempt, UPDATE_TOAST_DELAY_MS);
99
+ return () => clearTimeout(timer);
100
+ }, [advert, autoUpdateActive, suppressed, position]);
101
+ }
@@ -317,8 +317,16 @@ export function useVimSelection({
317
317
  return next;
318
318
  }, [containerRef, setState]);
319
319
 
320
+ // Whether vim was on since the last reset. The disabled branch below resets
321
+ // only when vim actually goes from on to off: on a mount where vim was never
322
+ // on there is nothing to reset, and `window.getSelection()` in
323
+ // `clearSelectionWithin` forces a layout on every document open.
324
+ const wasEnabledRef = useRef(enabled);
325
+
320
326
  useEffect(() => {
321
327
  if (!enabled) {
328
+ if (!wasEnabledRef.current) return;
329
+ wasEnabledRef.current = false;
322
330
  setState(createInitialVimSelectionState());
323
331
  setFocused(false);
324
332
  setHelpOpen(false);
@@ -326,6 +334,7 @@ export function useVimSelection({
326
334
  clearSelectionWithin(containerRef.current);
327
335
  return;
328
336
  }
337
+ wasEnabledRef.current = true;
329
338
  if (!focused) return;
330
339
  if (restoringFocusRef.current) {
331
340
  restoringFocusRef.current = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/ui",
3
- "version": "0.52.1",
3
+ "version": "0.53.1",
4
4
  "license": "Apache-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -77,7 +77,7 @@
77
77
  "@lezer/highlight": "^1.2.3",
78
78
  "@pierre/diffs": "1.3.6",
79
79
  "@plannotator/atomic-editor": "^0.9.0",
80
- "@plannotator/core": "0.25.12",
80
+ "@plannotator/core": "0.25.13",
81
81
  "@plannotator/markdown-editor": "^0.5.0",
82
82
  "@plannotator/web-highlighter": "^0.8.1",
83
83
  "@tanstack/react-table": "^8.21.3",
@@ -0,0 +1,18 @@
1
+ import { defineShortcutScope } from '../core';
2
+ import { createShortcutScopeHook } from '../runtime';
3
+
4
+ export const reviewMarkdownPreviewShortcuts = defineShortcutScope({
5
+ id: 'review-markdown-preview',
6
+ title: 'Markdown Files',
7
+ shortcuts: {
8
+ cycleMarkdownView: {
9
+ description: 'Show the current Markdown file as Source, Changes or Read (next view)',
10
+ bindings: ['P'],
11
+ section: 'Markdown Files',
12
+ hint: 'The open file, or in All files the file at the top of the view. New and deleted files have no Changes view. Markdown files you have not set by hand follow your choice.',
13
+ displayOrder: 10,
14
+ },
15
+ },
16
+ });
17
+
18
+ export const useReviewMarkdownPreviewShortcuts = createShortcutScopeHook(reviewMarkdownPreviewShortcuts);
@@ -34,3 +34,4 @@ export { reviewAllFilesDiffShortcuts, useReviewAllFilesDiffShortcuts } from './c
34
34
  export { reviewAiShortcuts, useReviewAiShortcuts } from './code-review/ai.shortcuts';
35
35
  export { reviewSuggestionModalShortcuts, useReviewSuggestionModalShortcuts } from './code-review/suggestionModal.shortcuts';
36
36
  export { reviewTourDialogShortcuts, useReviewTourDialogShortcuts } from './code-review/tourDialog.shortcuts';
37
+ export { reviewMarkdownPreviewShortcuts, useReviewMarkdownPreviewShortcuts } from './code-review/markdownPreview.shortcuts';
@@ -27,6 +27,12 @@ export interface UseShortcutScopeOptions<TScope extends ShortcutScopeDefinition<
27
27
  handlers: ShortcutHandlers<TScope>;
28
28
  target?: ShortcutEventTarget;
29
29
  stopOnMatch?: boolean;
30
+ /**
31
+ * Listen in the capture phase: the scope sees a key before the focused
32
+ * element does (a guard that must run before a component's own handler).
33
+ * Default false (bubble), so a component that used the key first can say so.
34
+ */
35
+ capture?: boolean;
30
36
  }
31
37
 
32
38
  interface ShortcutSequenceCandidate {
@@ -123,6 +129,7 @@ export function useShortcutScope<TScope extends ShortcutScopeDefinition<any>>({
123
129
  handlers,
124
130
  target = 'window',
125
131
  stopOnMatch = true,
132
+ capture = false,
126
133
  }: UseShortcutScopeOptions<TScope>) {
127
134
  const handlersRef = useRef(handlers);
128
135
  const sequenceRef = useRef<ShortcutSequenceState | null>(null);
@@ -206,11 +213,11 @@ export function useShortcutScope<TScope extends ShortcutScopeDefinition<any>>({
206
213
  if (started.length > 0) keyboardEvent.preventDefault();
207
214
  };
208
215
 
209
- eventTarget.addEventListener('keydown', handleKeyDown);
216
+ eventTarget.addEventListener('keydown', handleKeyDown, capture);
210
217
  return () => {
211
- eventTarget.removeEventListener('keydown', handleKeyDown);
218
+ eventTarget.removeEventListener('keydown', handleKeyDown, capture);
212
219
  };
213
- }, [scope, stopOnMatch, target]);
220
+ }, [scope, stopOnMatch, target, capture]);
214
221
  }
215
222
 
216
223
  export function createShortcutScopeHook<TScope extends ShortcutScopeDefinition<any>>(scope: TScope) {