@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.
Files changed (67) hide show
  1. package/HANDOFF.md +317 -29
  2. package/README.md +35 -3
  3. package/components/AnnotationPanel.tsx +159 -107
  4. package/components/AnnotationToolbar.tsx +17 -14
  5. package/components/AnnotationToolstrip.tsx +9 -0
  6. package/components/BlockRenderer.tsx +26 -14
  7. package/components/CodeFilePopout.tsx +131 -68
  8. package/components/CommentPopover.tsx +6 -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/ImageLightbox.tsx +288 -17
  13. package/components/ImagePeek.tsx +255 -0
  14. package/components/ImageResolverContext.tsx +31 -0
  15. package/components/InlineMarkdown.tsx +123 -15
  16. package/components/LocalLinkContext.tsx +49 -0
  17. package/components/MenuVersionSection.tsx +51 -25
  18. package/components/OpenInAppButton.tsx +18 -49
  19. package/components/PlanHeaderMenu.tsx +2 -8
  20. package/components/ProviderIcons.tsx +10 -4
  21. package/components/RenderedMarkdown.tsx +40 -4
  22. package/components/Settings.tsx +33 -5
  23. package/components/SkillReferenceMenu.tsx +28 -6
  24. package/components/Tooltip.tsx +51 -4
  25. package/components/Viewer.tsx +214 -55
  26. package/components/ai/AIProviderBar.tsx +11 -0
  27. package/components/ai/AskSessionSetupHint.tsx +12 -0
  28. package/components/ai/DocumentAIChatPanel.tsx +17 -5
  29. package/components/ai/SessionAskNotice.tsx +4 -3
  30. package/components/blocks/BlockquoteBlock.tsx +159 -0
  31. package/components/blocks/HtmlBlock.tsx +121 -9
  32. package/components/blocks/QuestionBlock.tsx +253 -104
  33. package/components/diagram/DiagramViewer.tsx +41 -4
  34. package/components/diagram/index.ts +1 -1
  35. package/components/html-viewer/HtmlViewer.tsx +73 -3
  36. package/components/html-viewer/bridge-script.asset.js +86 -0
  37. package/components/html-viewer/bridge-script.ts +86 -0
  38. package/components/html-viewer/useHtmlAnnotation.ts +51 -3
  39. package/components/icons/AgentIcons.tsx +9 -7
  40. package/components/plan-diff/PlanCleanDiffView.tsx +239 -36
  41. package/components/plan-diff/PlanDiffViewer.tsx +6 -1
  42. package/configure.ts +27 -1
  43. package/hooks/useAgentJobs.ts +8 -3
  44. package/hooks/useAnnotationHighlighter.ts +128 -23
  45. package/hooks/useExternalAnnotations.ts +6 -0
  46. package/hooks/useOpenInApps.ts +88 -0
  47. package/hooks/useUpdateCheck.ts +38 -98
  48. package/hooks/useUpdateCheckSetting.ts +43 -0
  49. package/hooks/useUpdateToast.ts +101 -0
  50. package/package.json +2 -2
  51. package/shortcuts/code-review/markdownPreview.shortcuts.ts +18 -0
  52. package/shortcuts/index.ts +1 -0
  53. package/styles.css +1 -1
  54. package/types.ts +48 -0
  55. package/utils/agentClosed.ts +26 -0
  56. package/utils/agentToolSetting.ts +16 -9
  57. package/utils/aiProvider.ts +7 -0
  58. package/utils/askSessionSetupHint.ts +27 -0
  59. package/utils/blockTargeting.ts +14 -3
  60. package/utils/parser.ts +65 -51
  61. package/utils/pasteInsert.ts +234 -0
  62. package/utils/planDiffEngine.ts +272 -2
  63. package/utils/skillCatalog.ts +15 -2
  64. package/utils/skillReferences.ts +50 -13
  65. package/utils/storage.ts +5 -0
  66. package/utils/textCut.ts +41 -0
  67. package/utils/updateNotice.ts +120 -0
@@ -1,3 +1,4 @@
1
+ import type { UpdateCheckSetting } from '../hooks/useUpdateCheckSetting';
1
2
  import React, { useState, useEffect, useMemo, useRef } from 'react';
2
3
  import { createPortal } from 'react-dom';
3
4
  import type { AnnotateAgentTerminalSide } from '@plannotator/core/agent-terminal';
@@ -112,6 +113,10 @@ interface SettingsProps {
112
113
  * `env` is PLANNOTATOR_AUTO_UPDATE when it overrides the config file.
113
114
  * Hosts that omit it get no toggle. */
114
115
  autoUpdateSetting?: { env?: boolean };
116
+ /** Present when the server reports the update-notice switch (serverConfig.updateCheck,
117
+ * useUpdateCheckSetting); `env` is PLANNOTATOR_UPDATE_CHECK when it overrides the
118
+ * config file. Hosts that omit it get no row. */
119
+ updateCheckSetting?: UpdateCheckSetting;
115
120
  /** The `plannotator` agent tool switch, as the server reports it
116
121
  * (parseAgentToolSetting of serverConfig: present only when the session's
117
122
  * host registers the tool). Shown only together with onAgentToolChange;
@@ -1002,9 +1007,10 @@ const CommentsTab: React.FC = () => {
1002
1007
  );
1003
1008
  };
1004
1009
 
1005
- export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange, onIdentityChange, origin, mode = 'plan', annotateParity = false, onUIPreferencesChange, externalOpen, onExternalClose, aiProviders = [], gitUser, autoUpdateSetting, agentToolSetting, onAgentToolChange, sinceBaseUnavailable, isCompactTouchLayout = false, onDetectObsidianVaults, agentTerminalAvailable = false, webmcpAvailable = false }) => {
1010
+ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange, onIdentityChange, origin, mode = 'plan', annotateParity = false, onUIPreferencesChange, externalOpen, onExternalClose, aiProviders = [], gitUser, autoUpdateSetting, updateCheckSetting, agentToolSetting, onAgentToolChange, sinceBaseUnavailable, isCompactTouchLayout = false, onDetectObsidianVaults, agentTerminalAvailable = false, webmcpAvailable = false }) => {
1006
1011
  const webmcpTools = useWebMcpToolsEnabled();
1007
1012
  const autoUpdate = useConfigValue('autoUpdate');
1013
+ const updateChecksOn = updateCheckSetting ? updateCheckSetting.on : true;
1008
1014
  const [showDialog, setShowDialog] = useState(false);
1009
1015
  const settingsWasOpenRef = useRef(false);
1010
1016
  const [themePreview, setThemePreview] = useState(false);
@@ -1441,20 +1447,42 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
1441
1447
  </div>
1442
1448
  </div>
1443
1449
 
1450
+ {/* Update notices: on by default. Writes updateCheck to
1451
+ ~/.plannotator/config.json; off makes no request to GitHub. */}
1452
+ {updateCheckSetting && (
1453
+ <>
1454
+ <div className="border-t border-border" />
1455
+ <ToggleSwitch
1456
+ checked={updateChecksOn}
1457
+ onChange={(v) => updateCheckSetting.onChange(v)}
1458
+ disabled={updateCheckSetting.env !== undefined}
1459
+ label="Tell me when a new version is out"
1460
+ description={
1461
+ updateCheckSetting.env !== undefined
1462
+ ? 'Set by PLANNOTATOR_UPDATE_CHECK in your environment.'
1463
+ : 'This computer asks GitHub once a day, never your browser. Off makes no request at all.'
1464
+ }
1465
+ />
1466
+ </>
1467
+ )}
1468
+
1444
1469
  {/* Auto-update (#1634): opt-in, default off. Writes
1445
- autoUpdate to ~/.plannotator/config.json. */}
1470
+ autoUpdate to ~/.plannotator/config.json. It needs
1471
+ update checks, so it is locked while they are off. */}
1446
1472
  {autoUpdateSetting && (
1447
1473
  <>
1448
1474
  <div className="border-t border-border" />
1449
1475
  <ToggleSwitch
1450
- checked={autoUpdateSetting.env ?? autoUpdate}
1476
+ checked={(autoUpdateSetting.env ?? autoUpdate) && updateChecksOn}
1451
1477
  onChange={(v) => configStore.set('autoUpdate', v)}
1452
- disabled={autoUpdateSetting.env !== undefined}
1478
+ disabled={autoUpdateSetting.env !== undefined || !updateChecksOn}
1453
1479
  label="Keep Plannotator up to date"
1454
1480
  description={
1455
1481
  autoUpdateSetting.env !== undefined
1456
1482
  ? 'Set by PLANNOTATOR_AUTO_UPDATE in your environment.'
1457
- : 'Installs new releases in the background, at most once a day, never while another review is open.'
1483
+ : !updateChecksOn
1484
+ ? 'Needs update checks (above).'
1485
+ : 'Installs new releases in the background, at most once a day, never while another review is open.'
1458
1486
  }
1459
1487
  />
1460
1488
  </>
@@ -182,11 +182,12 @@ export const SkillReferenceMenu: React.FC<SkillReferenceMenuProps> = ({
182
182
  }`}
183
183
  >
184
184
  <SkillIcon />
185
- <span className="shrink-0 font-semibold text-foreground">{item.name}</span>
186
- <span className="min-w-0 flex-1 truncate text-muted-foreground">
185
+ {/* data-skill-item-part: a host's styling hook (the Inbox lays a row out in two lines on a phone). */}
186
+ <span data-skill-item-part="name" className="shrink-0 font-semibold text-foreground">{item.name}</span>
187
+ <span data-skill-item-part="description" className="min-w-0 flex-1 truncate text-muted-foreground">
187
188
  {item.description ?? ''}
188
189
  </span>
189
- <span className="shrink-0 pl-3 text-xs text-muted-foreground/80">
190
+ <span data-skill-item-part="root" className="shrink-0 pl-3 text-xs text-muted-foreground/80">
190
191
  {ROOT_LABELS[item.root]}
191
192
  </span>
192
193
  </button>
@@ -215,6 +216,26 @@ const SkillIcon: React.FC = () => (
215
216
 
216
217
  interface HumanOnlySkillNoticeProps {
217
218
  skills: SkillCatalogEntry[];
219
+ /**
220
+ * What the instructions go with. Default: the host's (`setSkillInstructionsDestination`),
221
+ * else "your feedback". The Inbox says "your reply" or "your message".
222
+ */
223
+ destination?: string;
224
+ }
225
+
226
+ // Module-level seam (see packages/ui/CLAUDE.md): what every composer's
227
+ // human-only line says the instructions go with, unless the composer says.
228
+ // Plannotator's comments go with "your feedback"; the Inbox's comments on
229
+ // attached files ride a reply.
230
+ const DEFAULT_DESTINATION = 'your feedback';
231
+ let defaultDestination = DEFAULT_DESTINATION;
232
+
233
+ export function setSkillInstructionsDestination(destination: string): void {
234
+ defaultDestination = destination || DEFAULT_DESTINATION;
235
+ }
236
+
237
+ export function resetSkillInstructionsDestination(): void {
238
+ defaultDestination = DEFAULT_DESTINATION;
218
239
  }
219
240
 
220
241
  /**
@@ -227,8 +248,9 @@ interface HumanOnlySkillNoticeProps {
227
248
  * toggles), and assistive tech alike. It pairs with the dotted-underline
228
249
  * marker the composer overlay paints on human-only tokens.
229
250
  */
230
- export const HumanOnlySkillNotice: React.FC<HumanOnlySkillNoticeProps> = ({ skills }) => {
251
+ export const HumanOnlySkillNotice: React.FC<HumanOnlySkillNoticeProps> = ({ skills, destination }) => {
231
252
  if (skills.length === 0) return null;
253
+ destination ??= defaultDestination;
232
254
  const names = skills.map((s) => s.name).join(', ');
233
255
  return (
234
256
  <details data-skill-human-only-notice="true" className="group mt-1 px-1">
@@ -240,12 +262,12 @@ export const HumanOnlySkillNotice: React.FC<HumanOnlySkillNoticeProps> = ({ skil
240
262
  {skills.length === 1 ? (
241
263
  <>
242
264
  <span className="font-mono">{names}</span> cannot be invoked by a model, so
243
- its instructions will be included with your feedback.
265
+ its instructions will be included with {destination}.
244
266
  </>
245
267
  ) : (
246
268
  <>
247
269
  <span className="font-mono">{names}</span> cannot be invoked by a model, so
248
- their instructions will be included with your feedback.
270
+ their instructions will be included with {destination}.
249
271
  </>
250
272
  )}
251
273
  </p>
@@ -44,6 +44,25 @@ interface TooltipProps {
44
44
  * that use it for short button labels.
45
45
  */
46
46
  wide?: boolean;
47
+ /**
48
+ * Asked each time the tooltip would open, with the trigger element; `false`
49
+ * keeps it closed this time (e.g. open only while the trigger's text is cut
50
+ * off). Unset: it always opens, as before.
51
+ */
52
+ shouldOpen?: (trigger: Element) => boolean;
53
+ /**
54
+ * Where the popup is portaled. Unset: the document body. A trigger inside a
55
+ * native modal `<dialog>` needs the dialog here, or the popup is drawn
56
+ * under the dialog's top layer.
57
+ */
58
+ container?: HTMLElement | null;
59
+ /**
60
+ * When true, Escape closes the tooltip without using the key up: it still
61
+ * reaches the page (not default-prevented, not stopped), so the page's own
62
+ * Escape (closing a pane) works while a tooltip is open. Unset: Base UI's
63
+ * default, where the Escape that closes a tooltip goes no further.
64
+ */
65
+ escapePassesThrough?: boolean;
47
66
  }
48
67
 
49
68
  export const Tooltip: React.FC<TooltipProps> = ({
@@ -54,15 +73,43 @@ export const Tooltip: React.FC<TooltipProps> = ({
54
73
  delayDuration,
55
74
  sideOffset = 8,
56
75
  wide = false,
76
+ shouldOpen,
77
+ container,
78
+ escapePassesThrough = false,
57
79
  }) => {
58
80
  const disableHoverablePopup = React.useContext(DisableHoverablePopupContext);
81
+ // Controlled only for escapePassesThrough: the Escape close is canceled (so Base UI neither prevents nor stops
82
+ // the key) and the tooltip is closed here instead.
83
+ const [controlledOpen, setControlledOpen] = React.useState(false);
84
+ const controlled = escapePassesThrough ? { open: controlledOpen } : {};
59
85
  return (
60
- <BaseTooltip.Root disableHoverablePopup={disableHoverablePopup}>
86
+ <BaseTooltip.Root
87
+ disableHoverablePopup={disableHoverablePopup}
88
+ {...controlled}
89
+ onOpenChange={
90
+ shouldOpen || escapePassesThrough
91
+ ? (open, details) => {
92
+ if (open && shouldOpen && !(details.trigger && shouldOpen(details.trigger))) {
93
+ details.cancel();
94
+ return;
95
+ }
96
+ if (!escapePassesThrough) return;
97
+ if (!open && details.reason === 'escape-key') {
98
+ details.cancel();
99
+ details.allowPropagation();
100
+ }
101
+ setControlledOpen(open);
102
+ }
103
+ : undefined
104
+ }
105
+ >
61
106
  <BaseTooltip.Trigger render={children} delay={delayDuration} />
62
- <BaseTooltip.Portal>
63
- <BaseTooltip.Positioner side={side} align={align} sideOffset={sideOffset} className="isolate z-50">
107
+ <BaseTooltip.Portal container={container ?? undefined}>
108
+ {/* Above the annotation toolbar (z-[100]), so a tooltip that flips toward it is never tucked under it. */}
109
+ <BaseTooltip.Positioner side={side} align={align} sideOffset={sideOffset} className="isolate z-[110]">
64
110
  <BaseTooltip.Popup
65
- className={`z-50 px-2 py-1 text-xs bg-popover text-popover-foreground border border-border rounded shadow-md origin-[var(--transform-origin)] transition-[opacity,scale] duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 ${
111
+ data-pn-tooltip=""
112
+ className={`z-[110] px-2 py-1 text-xs bg-popover text-popover-foreground border border-border rounded shadow-md origin-[var(--transform-origin)] transition-[opacity,scale] duration-150 ease-out data-[starting-style]:opacity-0 data-[starting-style]:scale-95 data-[ending-style]:opacity-0 data-[ending-style]:scale-95 ${
66
113
  wide
67
114
  ? 'max-w-[min(260px,calc(100vw-1rem))] leading-snug whitespace-normal [overflow-wrap:anywhere]'
68
115
  : 'whitespace-nowrap'