@plannotator/ui 0.30.0 → 0.32.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/README.md +46 -1
- package/components/ActionMenu.tsx +6 -1
- package/components/AgentsTab.tsx +8 -9
- package/components/AnalysisLayerToggle.tsx +48 -0
- package/components/AnnotationPanel.tsx +158 -36
- package/components/AnnotationToolbar.tsx +50 -30
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/CommentPopover.tsx +238 -46
- package/components/ConfirmDialog.tsx +42 -28
- package/components/GraphvizBlock.tsx +86 -7
- package/components/HtmlSurfaceControls.tsx +170 -0
- package/components/InlineMarkdown.tsx +25 -4
- package/components/KeyboardShortcuts.tsx +9 -0
- package/components/Landing.tsx +1 -1
- package/components/LookAndFeelAnnouncementDialog.tsx +147 -178
- package/components/MarkdownEditor/embedPicker.ts +349 -0
- package/components/MarkdownEditor.tsx +12 -0
- package/components/MermaidBlock.tsx +60 -26
- package/components/ModeToggle.tsx +2 -1
- package/components/PermissionModeSetup.tsx +24 -5
- package/components/PinpointOverlay.tsx +11 -6
- package/components/PlanHeaderMenu.tsx +140 -1
- package/components/SearchableSelect.tsx +2 -0
- package/components/Settings.tsx +175 -10
- package/components/SkillReferenceMenu.tsx +9 -0
- package/components/StickyHeaderLane.tsx +9 -2
- package/components/TableOfContents.tsx +9 -4
- package/components/TextShimmer.tsx +8 -5
- package/components/ThemeProvider.tsx +43 -1
- package/components/ThemeTab.tsx +52 -1
- package/components/Tooltip.tsx +3 -1
- package/components/Viewer.tsx +19 -5
- package/components/VimTargetReticle.tsx +12 -4
- package/components/ai/DocumentAIChatPanel.tsx +1 -0
- package/components/blocks/MathBlock.tsx +26 -14
- package/components/core/button.tsx +14 -6
- package/components/html-viewer/HtmlViewer.tsx +320 -41
- package/components/html-viewer/bridge-script.ts +360 -72
- package/components/html-viewer/composerYield.ts +1 -51
- package/components/html-viewer/hostThreads.ts +37 -0
- package/components/html-viewer/index.ts +9 -0
- package/components/html-viewer/unanchored.ts +47 -0
- package/components/html-viewer/useHtmlAnnotation.ts +240 -61
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -0
- package/components/sidebar/FileBrowser.tsx +17 -5
- package/components/sidebar/SidebarContainer.tsx +124 -28
- package/components/ui/button.tsx +10 -8
- package/components/ui/dialog.tsx +35 -25
- package/config/index.ts +6 -1
- package/config/reviewView.ts +42 -9
- package/config/settings.ts +141 -0
- package/configure.ts +32 -0
- package/hooks/useAIProviderConfig.ts +8 -7
- package/hooks/useActiveSection.ts +6 -4
- package/hooks/useAgentJobs.ts +3 -0
- package/hooks/useAnnotationHighlighter.ts +20 -0
- package/hooks/useHtmlRefresh.ts +149 -0
- package/hooks/useIsMobile.ts +37 -0
- package/hooks/useLinkedDoc.ts +7 -0
- package/hooks/useMathRenderer.ts +30 -0
- package/hooks/useScrollViewport.ts +74 -0
- package/hooks/useSharing.ts +31 -5
- package/hooks/useViewportEnvironment.ts +350 -0
- package/package.json +5 -2
- package/shortcuts/index.ts +3 -0
- package/shortcuts/plan-review/annotationMode.shortcuts.ts +91 -0
- package/shortcuts/plan-review/documentView.shortcuts.ts +26 -0
- package/shortcuts/plan-review/htmlAnnotate.shortcuts.ts +24 -0
- package/styles.css +1 -1
- package/theme.css +229 -0
- package/types.ts +35 -0
- package/utils/annotateAgentTerminal.ts +36 -5
- package/utils/blockTargeting.ts +6 -3
- package/utils/composerYield.ts +45 -0
- package/utils/generateIdentity.ts +64 -14
- package/utils/htmlChrome.ts +20 -16
- package/utils/identity-tater.ts +36 -0
- package/utils/lookAndFeelAnnouncement.ts +12 -8
- package/utils/markdownExtensions.ts +57 -0
- package/utils/math-eager.ts +25 -0
- package/utils/math.ts +146 -0
- package/utils/mermaid-eager.ts +28 -0
- package/utils/mermaid.ts +132 -0
- package/utils/parser.ts +75 -2
- package/utils/quickLabels.ts +13 -0
- package/utils/vimNavigation.ts +4 -1
- package/utils/vimScroll.ts +9 -4
- package/utils/wideMode.ts +20 -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
- package/components/PlanAIAnnouncementDialog.tsx +0 -187
- package/components/VimModeAnnouncementDialog.tsx +0 -557
- package/utils/planAIAnnouncement.ts +0 -17
- package/utils/vimModeAnnouncement.ts +0 -23
package/theme.css
CHANGED
|
@@ -627,6 +627,14 @@
|
|
|
627
627
|
* the surface/sidebar-aware components expect. Themes that pin explicit
|
|
628
628
|
* values (e.g. "simple") override these. */
|
|
629
629
|
:root {
|
|
630
|
+
/* Mobile browser environment. Safe areas are CSS-owned because they are
|
|
631
|
+
* available before React mounts; the observed viewport values are written
|
|
632
|
+
* by useViewportEnvironment and fall back cleanly in the utilities below. */
|
|
633
|
+
--pn-safe-top: env(safe-area-inset-top, 0px);
|
|
634
|
+
--pn-safe-right: env(safe-area-inset-right, 0px);
|
|
635
|
+
--pn-safe-bottom: env(safe-area-inset-bottom, 0px);
|
|
636
|
+
--pn-safe-left: env(safe-area-inset-left, 0px);
|
|
637
|
+
--pn-touch-target: 2.75rem;
|
|
630
638
|
--surface-0: color-mix(in oklch, var(--muted) 40%, var(--background));
|
|
631
639
|
--surface-1: var(--muted);
|
|
632
640
|
--surface-2: color-mix(in oklch, var(--muted) 70%, var(--foreground));
|
|
@@ -704,6 +712,227 @@ body {
|
|
|
704
712
|
font-feature-settings: "ss01", "ss02", "cv01";
|
|
705
713
|
}
|
|
706
714
|
|
|
715
|
+
/* Safari extends the page canvas beneath its floating browser controls. The
|
|
716
|
+
* plan document is a card-colored nested scroller, so leaving the outer canvas
|
|
717
|
+
* on `--background` produces an opaque dark band around those controls. Match
|
|
718
|
+
* the browser canvas to the active surface while preserving the incumbent
|
|
719
|
+
* background for grid, HTML, Review, loading, and non-editor consumers. */
|
|
720
|
+
html:has([data-pn-browser-canvas="card"]),
|
|
721
|
+
body:has([data-pn-browser-canvas="card"]) {
|
|
722
|
+
background-color: var(--card);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
html:has([data-pn-browser-canvas="background"]),
|
|
726
|
+
body:has([data-pn-browser-canvas="background"]) {
|
|
727
|
+
background-color: var(--background);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
/* Mobile Safari only collapses its browser controls for page scrolling; it
|
|
731
|
+
* deliberately leaves them expanded when a gesture scrolls a nested element.
|
|
732
|
+
* Plan opts compact coarse-pointer layouts into document scrolling while the
|
|
733
|
+
* fixed application-stage architecture remains unchanged everywhere else. */
|
|
734
|
+
html:has([data-pn-document-scroll="true"]),
|
|
735
|
+
body:has([data-pn-document-scroll="true"]) {
|
|
736
|
+
height: auto;
|
|
737
|
+
overflow-x: hidden;
|
|
738
|
+
overflow-y: auto;
|
|
739
|
+
overscroll-behavior: auto;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
body:has([data-pn-document-scroll="true"]) > #root {
|
|
743
|
+
height: auto;
|
|
744
|
+
overflow: visible;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
.pn-app-viewport[data-pn-document-scroll="true"] {
|
|
748
|
+
height: auto;
|
|
749
|
+
min-height: 100vh;
|
|
750
|
+
min-height: 100dvh;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* The application stage owns the top and lateral safe areas. Bottom-edge
|
|
754
|
+
* surfaces opt into `.pn-safe-bottom` themselves so the home-indicator inset
|
|
755
|
+
* is never counted twice. The ordered height declarations preserve desktop
|
|
756
|
+
* behavior and old-browser support before the observer publishes a value. */
|
|
757
|
+
.pn-app-viewport {
|
|
758
|
+
box-sizing: border-box;
|
|
759
|
+
height: 100vh;
|
|
760
|
+
height: 100dvh;
|
|
761
|
+
height: var(--pn-viewport-height, 100vh);
|
|
762
|
+
padding-top: var(--pn-safe-top);
|
|
763
|
+
padding-right: var(--pn-safe-right);
|
|
764
|
+
padding-left: var(--pn-safe-left);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
.pn-safe-top {
|
|
768
|
+
padding-top: var(--pn-safe-top);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
.pn-safe-inline {
|
|
772
|
+
padding-right: var(--pn-safe-right);
|
|
773
|
+
padding-left: var(--pn-safe-left);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
.pn-safe-bottom {
|
|
777
|
+
padding-bottom: var(--pn-safe-bottom);
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
.pn-viewport-bounded {
|
|
781
|
+
max-height: calc(
|
|
782
|
+
var(--pn-viewport-height, 100vh) - var(--pn-safe-top) - var(--pn-safe-bottom)
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/* Portaled overlays cannot inherit the application root's safe-area padding.
|
|
787
|
+
* This utility gives them the same observed visible stage without assuming
|
|
788
|
+
* that the software keyboard is a rectangular bottom inset. */
|
|
789
|
+
.pn-visible-viewport-overlay {
|
|
790
|
+
position: fixed;
|
|
791
|
+
top: var(--pn-viewport-offset-top, 0px);
|
|
792
|
+
left: var(--pn-viewport-offset-left, 0px);
|
|
793
|
+
box-sizing: border-box;
|
|
794
|
+
width: var(--pn-viewport-width, 100vw);
|
|
795
|
+
height: var(--pn-viewport-height, 100vh);
|
|
796
|
+
padding-top: max(1rem, var(--pn-safe-top));
|
|
797
|
+
padding-right: max(1rem, var(--pn-safe-right));
|
|
798
|
+
padding-bottom: max(1rem, var(--pn-safe-bottom));
|
|
799
|
+
padding-left: max(1rem, var(--pn-safe-left));
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/* Full-stage compact surfaces (for example the Plan navigator) use the same
|
|
803
|
+
* observed visual viewport as portaled dialogs without inheriting their 1rem
|
|
804
|
+
* ornamental margin. The stage owns its safe areas and contains scroll-chain
|
|
805
|
+
* gestures so the document beneath it does not move at a list boundary. */
|
|
806
|
+
.pn-visible-viewport-stage {
|
|
807
|
+
position: fixed;
|
|
808
|
+
top: var(--pn-viewport-offset-top, 0px);
|
|
809
|
+
left: var(--pn-viewport-offset-left, 0px);
|
|
810
|
+
box-sizing: border-box;
|
|
811
|
+
width: var(--pn-viewport-width, 100vw);
|
|
812
|
+
height: var(--pn-viewport-height, 100vh);
|
|
813
|
+
padding-top: var(--pn-safe-top);
|
|
814
|
+
padding-right: var(--pn-safe-right);
|
|
815
|
+
padding-bottom: var(--pn-safe-bottom);
|
|
816
|
+
padding-left: var(--pn-safe-left);
|
|
817
|
+
overscroll-behavior: contain;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/* The navigator reuses information-dense desktop browser content, but its
|
|
821
|
+
* interactive rows become physical controls on touch. Scope both the 44px
|
|
822
|
+
* target and the TOC's legible type lift to the compact stage only. */
|
|
823
|
+
[data-pn-plan-navigator="true"] button {
|
|
824
|
+
min-block-size: var(--pn-touch-target);
|
|
825
|
+
touch-action: manipulation;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
[data-pn-plan-navigator="true"] nav button {
|
|
829
|
+
font-size: 0.8125rem;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
[data-pn-plan-navigator="true"] .file-browser-filter-field {
|
|
833
|
+
height: var(--pn-touch-target);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
[data-pn-plan-navigator="true"] .file-browser-filter-clear {
|
|
837
|
+
min-inline-size: var(--pn-touch-target);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
/* Compact Plan auxiliary stages reuse dense desktop panel contents, but every
|
|
841
|
+
* nested action remains a physical touch control. This selector is stage-only,
|
|
842
|
+
* so the incumbent desktop panel geometry stays pixel-identical. */
|
|
843
|
+
[data-pn-compact-plan-stage="true"] button {
|
|
844
|
+
min-block-size: var(--pn-touch-target);
|
|
845
|
+
touch-action: manipulation;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/* Existing expanded composers keep their 85dvh desktop proportion. Compact
|
|
849
|
+
* and touch layouts use the full safe visible stage so the keyboard cannot
|
|
850
|
+
* strand the footer below an ornamental desktop margin. */
|
|
851
|
+
.pn-responsive-composer-dialog {
|
|
852
|
+
width: 100%;
|
|
853
|
+
height: min(36rem, 85vh);
|
|
854
|
+
height: min(36rem, 85dvh);
|
|
855
|
+
max-width: 42rem;
|
|
856
|
+
max-height: 100%;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
@media (pointer: coarse), (max-width: 639px), (max-height: 419px) {
|
|
860
|
+
.pn-responsive-composer-dialog {
|
|
861
|
+
height: 100%;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/* Code review often promotes a line selection straight into the expanded
|
|
865
|
+
* composer. Keep its idle surface proportional to the content instead of
|
|
866
|
+
* covering the whole phone; once the keyboard reduces the visible viewport,
|
|
867
|
+
* the same cap naturally yields to all available space. Plan's explicit
|
|
868
|
+
* expanded-composer treatment remains unchanged. */
|
|
869
|
+
.pn-responsive-composer-dialog.pn-review-composer-dialog {
|
|
870
|
+
height: min(28rem, 100%);
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
/* iOS zooms focused text controls below 16px. Scope the correction to
|
|
875
|
+
* user-authored primary inputs so labels, counters, and diff text stay at the
|
|
876
|
+
* established desktop scale. The mirror selector keeps skill-token glyphs
|
|
877
|
+
* metrically identical to the transparent textarea above it. */
|
|
878
|
+
@media (pointer: coarse), (max-width: 639px) {
|
|
879
|
+
[data-pn-mobile-editable],
|
|
880
|
+
[data-pn-mobile-editable-mirror] {
|
|
881
|
+
font-size: 1rem !important;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/* Shared controls keep their incumbent visual density but gain a physical
|
|
886
|
+
* 44px target inside the canonical compact touch shell. The root marker comes
|
|
887
|
+
* from the same JS media query that owns scroll and shell composition, so a
|
|
888
|
+
* secondary touchscreen cannot reclassify a fine-primary-pointer desktop.
|
|
889
|
+
* Selecting from `html` also reaches portaled controls outside the app root. */
|
|
890
|
+
html:has([data-pn-compact-touch-layout='true']) [data-pn-touch-target] {
|
|
891
|
+
min-block-size: var(--pn-touch-target);
|
|
892
|
+
touch-action: manipulation;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
html:has([data-pn-compact-touch-layout='true'])
|
|
896
|
+
[data-pn-touch-target][data-pn-touch-target-icon] {
|
|
897
|
+
min-inline-size: var(--pn-touch-target);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
html:has([data-pn-compact-touch-layout='true'])
|
|
901
|
+
[data-pn-compact-diff-options]
|
|
902
|
+
button {
|
|
903
|
+
min-block-size: var(--pn-touch-target);
|
|
904
|
+
touch-action: manipulation;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/* The selection toolbar packs its controls at desktop density (2px gaps), so
|
|
908
|
+
* 44px targets alone would still leave adjacent destructive and comment
|
|
909
|
+
* actions a mis-tap apart. Space them only inside the compact scope. */
|
|
910
|
+
html:has([data-pn-compact-touch-layout='true'])
|
|
911
|
+
[data-pn-annotation-toolbar-row] {
|
|
912
|
+
gap: 0.5rem;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
html:has([data-pn-compact-touch-layout='true'])
|
|
916
|
+
[data-pn-touch-target]:not(:disabled):not([aria-disabled='true']):active {
|
|
917
|
+
filter: brightness(0.92);
|
|
918
|
+
transition: filter 100ms ease-out;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
@media (prefers-reduced-motion: reduce) {
|
|
922
|
+
html:has([data-pn-compact-touch-layout='true'])
|
|
923
|
+
[data-pn-touch-target]:not(:disabled):not([aria-disabled='true']):active {
|
|
924
|
+
filter: brightness(0.9);
|
|
925
|
+
transition: none;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
@media (hover: hover) and (pointer: fine) {
|
|
930
|
+
.plan-look-choice-option[aria-pressed='false']:hover {
|
|
931
|
+
border-color: color-mix(in srgb, var(--muted-foreground) 40%, transparent);
|
|
932
|
+
background: color-mix(in srgb, var(--muted) 35%, transparent);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
707
936
|
.math-inline {
|
|
708
937
|
display: inline-flex;
|
|
709
938
|
align-items: center;
|
package/types.ts
CHANGED
|
@@ -79,6 +79,8 @@ export interface Annotation {
|
|
|
79
79
|
displayMode: boolean;
|
|
80
80
|
}>; // math elements covered by a mixed text+formula selection
|
|
81
81
|
prUrl?: string; // code-review PR mode: the PR this note belongs to, so it isn't shown/exported against another PR after an in-place switch
|
|
82
|
+
pageUrl?: string; // set only by live app annotate sessions: the page (pathname + search) the annotation was made on; restore filters to the current page and export groups by page
|
|
83
|
+
inReplyTo?: string; // id of the annotation this one replies to; a reply inherits its parent's anchor, renders indented under it in the panel, and exports grouped under it. Additive: annotations without it render and export exactly as before.
|
|
82
84
|
htmlAnchor?: HtmlElementAnchor; // raw-HTML pinpoint: serialized element anchor for reliable restoration
|
|
83
85
|
htmlAdditionalTargets?: HtmlAnnotationTarget[]; // raw-HTML shift-click multi-select: extra elements this one comment covers (primary stays htmlAnchor/originalText)
|
|
84
86
|
// web-highlighter metadata for cross-element selections
|
|
@@ -155,6 +157,32 @@ export type CodeAnnotationType = 'comment' | 'suggestion' | 'concern';
|
|
|
155
157
|
// must branch on scope, never read those sentinels as a real path or row.
|
|
156
158
|
export type CodeAnnotationScope = 'line' | 'file' | 'general';
|
|
157
159
|
|
|
160
|
+
/**
|
|
161
|
+
* One inferred step selected from the Call Flow analysis surface.
|
|
162
|
+
*
|
|
163
|
+
* Source location is optional because CallDiff can surface structural steps
|
|
164
|
+
* without a concrete line. `CodeAnnotation` uses an in-patch located target
|
|
165
|
+
* as its native inline anchor when one exists; otherwise the annotation is
|
|
166
|
+
* file- or review-scoped while this target remains its durable Call Flow
|
|
167
|
+
* anchor. Raw output selections use a one-based `rawLine` instead of a source
|
|
168
|
+
* location. This lets every rendered Call Flow row and raw line participate in
|
|
169
|
+
* feedback without pretending an out-of-hunk or diagnostic line can be posted
|
|
170
|
+
* as an inline source comment.
|
|
171
|
+
*/
|
|
172
|
+
interface CallFlowAnnotationTargetBase {
|
|
173
|
+
treePath: string;
|
|
174
|
+
entry: string;
|
|
175
|
+
label: string;
|
|
176
|
+
side: 'old' | 'new';
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export type CallFlowAnnotationTarget = CallFlowAnnotationTargetBase & (
|
|
180
|
+
| { filePath: string; lineStart: number; lineEnd: number; rawLine?: undefined }
|
|
181
|
+
| { filePath: string; lineStart?: undefined; lineEnd?: undefined; rawLine?: undefined }
|
|
182
|
+
| { rawLine: number; filePath?: undefined; lineStart?: undefined; lineEnd?: undefined }
|
|
183
|
+
| { rawLine?: undefined; filePath?: undefined; lineStart?: undefined; lineEnd?: undefined }
|
|
184
|
+
);
|
|
185
|
+
|
|
158
186
|
/** Conventional Comments label — see https://conventionalcomments.org */
|
|
159
187
|
export type ConventionalLabel =
|
|
160
188
|
| 'praise'
|
|
@@ -214,6 +242,13 @@ export interface CodeAnnotation {
|
|
|
214
242
|
* line anchor maps to the pristine lines those edits replace, so it is
|
|
215
243
|
* approximate and the export labels it as such. */
|
|
216
244
|
selectedTextFromEdits?: boolean;
|
|
245
|
+
/**
|
|
246
|
+
* Complete Call Flow selection for an annotation authored from that
|
|
247
|
+
* surface. When any target maps to the patch, one target also supplies this
|
|
248
|
+
* annotation's primary inline anchor; otherwise the annotation is file- or
|
|
249
|
+
* review-scoped. Target order always preserves the user's selection order.
|
|
250
|
+
*/
|
|
251
|
+
callFlowTargets?: CallFlowAnnotationTarget[];
|
|
217
252
|
createdAt: number;
|
|
218
253
|
author?: string;
|
|
219
254
|
source?: string; // External tool identifier (e.g., "eslint") — set when annotation comes from external API
|
|
@@ -1,14 +1,45 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
AgentTerminalAgent,
|
|
3
|
+
AnnotateAgentTerminalSide,
|
|
4
|
+
} from "@plannotator/core/agent-terminal";
|
|
5
|
+
import { configStore } from "../config";
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
// The side/placement vocabulary lives in @plannotator/core so the settings
|
|
8
|
+
// registry can reach it without importing this module (which would close a
|
|
9
|
+
// cycle through ConfigStore). Re-exported here because this is the seam the
|
|
10
|
+
// editor package imports from.
|
|
11
|
+
export {
|
|
12
|
+
ANNOTATE_AGENT_TERMINAL_SIDES,
|
|
13
|
+
isAnnotateAgentTerminalSide,
|
|
14
|
+
resolveAnnotateAgentTerminalPlacement,
|
|
15
|
+
resolveAnnotateAgentTerminalSide,
|
|
16
|
+
} from "@plannotator/core/agent-terminal";
|
|
17
|
+
export type {
|
|
18
|
+
AnnotateAgentTerminalPlacement,
|
|
19
|
+
AnnotateAgentTerminalSide,
|
|
20
|
+
} from "@plannotator/core/agent-terminal";
|
|
5
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Both Agent TUI preferences resolve through ConfigStore (server config file >
|
|
24
|
+
* cookie > default) rather than reading cookies directly. Annotate sessions run
|
|
25
|
+
* on a fresh random port every time, so a cookie is scoped to a single session;
|
|
26
|
+
* the server round-trip through ~/.plannotator/config.json is what makes these
|
|
27
|
+
* choices durable. Same seam identity.ts uses for `displayName`.
|
|
28
|
+
*/
|
|
6
29
|
export function getSavedAnnotateAgentId(): string | null {
|
|
7
|
-
return
|
|
30
|
+
return configStore.get("agentTerminalDefaultAgent") || null;
|
|
8
31
|
}
|
|
9
32
|
|
|
10
33
|
export function saveAnnotateAgentId(agentId: string): void {
|
|
11
|
-
|
|
34
|
+
configStore.set("agentTerminalDefaultAgent", agentId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getSavedAnnotateAgentTerminalSide(): AnnotateAgentTerminalSide {
|
|
38
|
+
return configStore.get("agentTerminalSide");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function saveAnnotateAgentTerminalSide(side: AnnotateAgentTerminalSide): void {
|
|
42
|
+
configStore.set("agentTerminalSide", side);
|
|
12
43
|
}
|
|
13
44
|
|
|
14
45
|
export function resolveAnnotateAgentId(
|
package/utils/blockTargeting.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
+
import { getScrollViewportRect } from '../hooks/useScrollViewport';
|
|
2
|
+
import { createTextRange } from './domSelection';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Semantic document targeting shared by pointer Pinpoint and Vim navigation.
|
|
3
6
|
*
|
|
4
7
|
* The graph is rebuilt from the live rendered document whenever a consumer
|
|
5
8
|
* needs it. Callers persist stable keys, never DOM nodes, across renders.
|
|
6
9
|
*/
|
|
7
|
-
import { createTextRange } from './domSelection';
|
|
8
|
-
|
|
9
10
|
/** Elements that never participate in document targeting. */
|
|
10
11
|
const SKIP_SELECTORS = [
|
|
11
12
|
'.annotation-toolbar',
|
|
@@ -362,7 +363,9 @@ export function findInitialSemanticTarget(
|
|
|
362
363
|
graph: SemanticTargetGraph,
|
|
363
364
|
scrollViewport?: HTMLElement | null,
|
|
364
365
|
): SemanticTarget | null {
|
|
365
|
-
const viewportRect =
|
|
366
|
+
const viewportRect = scrollViewport
|
|
367
|
+
? getScrollViewportRect(scrollViewport)
|
|
368
|
+
: graph.container.getBoundingClientRect();
|
|
366
369
|
const centerY = viewportRect.top + viewportRect.height / 2;
|
|
367
370
|
return graph.blockKeys
|
|
368
371
|
.map((key) => resolveSemanticTarget(graph, key))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composer-yield state machine for shift-click multi-select.
|
|
3
|
+
*
|
|
4
|
+
* While Shift is held during a multi-target draft, the comment composer gets
|
|
5
|
+
* out of the pointer's way: it fades as the pointer approaches and becomes
|
|
6
|
+
* click-through when the pointer is over it. Restoration uses hysteresis so
|
|
7
|
+
* the composer does not flicker at its boundary.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export type ComposerYieldState = 'none' | 'near' | 'over';
|
|
11
|
+
|
|
12
|
+
/** Distance (px from the composer rect) at which the composer starts fading. */
|
|
13
|
+
export const COMPOSER_YIELD_NEAR_ENTER_PX = 80;
|
|
14
|
+
/** Distance the pointer must exceed before a faded composer fully restores. */
|
|
15
|
+
export const COMPOSER_YIELD_NEAR_EXIT_PX = 96;
|
|
16
|
+
/** Distance the pointer must clear a click-through composer by to restore. */
|
|
17
|
+
export const COMPOSER_YIELD_OVER_EXIT_PX = 48;
|
|
18
|
+
|
|
19
|
+
/** Shortest distance from a point to a rect's edge; <= 0 means inside. */
|
|
20
|
+
export function distanceToRect(
|
|
21
|
+
x: number,
|
|
22
|
+
y: number,
|
|
23
|
+
rect: { left: number; top: number; right: number; bottom: number },
|
|
24
|
+
): number {
|
|
25
|
+
const dx = Math.max(rect.left - x, 0, x - rect.right);
|
|
26
|
+
const dy = Math.max(rect.top - y, 0, y - rect.bottom);
|
|
27
|
+
if (dx === 0 && dy === 0) return 0;
|
|
28
|
+
return Math.hypot(dx, dy);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Next yield state for a pointer at `distance` px from the composer rect. */
|
|
32
|
+
export function computeComposerYield(
|
|
33
|
+
previous: ComposerYieldState,
|
|
34
|
+
distance: number,
|
|
35
|
+
): ComposerYieldState {
|
|
36
|
+
if (distance <= 0) return 'over';
|
|
37
|
+
if (previous === 'over') {
|
|
38
|
+
if (distance <= COMPOSER_YIELD_OVER_EXIT_PX) return 'over';
|
|
39
|
+
return distance <= COMPOSER_YIELD_NEAR_EXIT_PX ? 'near' : 'none';
|
|
40
|
+
}
|
|
41
|
+
if (previous === 'near') {
|
|
42
|
+
return distance <= COMPOSER_YIELD_NEAR_EXIT_PX ? 'near' : 'none';
|
|
43
|
+
}
|
|
44
|
+
return distance <= COMPOSER_YIELD_NEAR_ENTER_PX ? 'near' : 'none';
|
|
45
|
+
}
|
|
@@ -1,12 +1,73 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tater name generator
|
|
2
|
+
* Tater name generator: pure function, no storage dependencies.
|
|
3
3
|
*
|
|
4
4
|
* Extracted to its own module to avoid circular imports:
|
|
5
5
|
* settings.ts needs this for the default value, and identity.ts
|
|
6
6
|
* needs configStore (which imports settings.ts).
|
|
7
|
+
*
|
|
8
|
+
* The full `unique-username-generator` dictionary is NOT imported here. It is
|
|
9
|
+
* registered into the slot below by `./identity-tater`, which every
|
|
10
|
+
* Plannotator entry imports eagerly, so Plannotator mints names from the
|
|
11
|
+
* full dictionary exactly as before. A host that provides its own
|
|
12
|
+
* `identityProvider` never calls the generator and, with the static import
|
|
13
|
+
* gone, no longer ships the word lists.
|
|
14
|
+
*
|
|
15
|
+
* The slot is SYNCHRONOUS on purpose: `configStore.ensureLoaded()` evaluates
|
|
16
|
+
* this default during the first settings read (a render-time read) and
|
|
17
|
+
* persists the result to the identity cookie at once, so a name that arrived
|
|
18
|
+
* later would be a visible identity change. The built-in fallback below keeps
|
|
19
|
+
* the same `{adjective}-{noun}-tater` shape from a small inline pool.
|
|
7
20
|
*/
|
|
8
21
|
|
|
9
|
-
|
|
22
|
+
export type IdentityGenerator = () => string;
|
|
23
|
+
|
|
24
|
+
const FALLBACK_ADJECTIVES = [
|
|
25
|
+
'swift', 'gentle', 'brave', 'calm', 'clever', 'bright', 'quiet', 'bold',
|
|
26
|
+
'eager', 'kind', 'lucky', 'merry', 'nimble', 'proud', 'sunny', 'witty',
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
const FALLBACK_NOUNS = [
|
|
30
|
+
'falcon', 'crystal', 'river', 'meadow', 'harbor', 'comet', 'maple', 'otter',
|
|
31
|
+
'summit', 'lantern', 'willow', 'ember', 'pebble', 'breeze', 'orchid', 'canyon',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/** Pool words: exported for tests only. */
|
|
35
|
+
export const FALLBACK_IDENTITY_POOL: {
|
|
36
|
+
readonly adjectives: readonly string[];
|
|
37
|
+
readonly nouns: readonly string[];
|
|
38
|
+
} = {
|
|
39
|
+
adjectives: FALLBACK_ADJECTIVES,
|
|
40
|
+
nouns: FALLBACK_NOUNS,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function pick<T>(list: readonly T[]): T {
|
|
44
|
+
return list[Math.floor(Math.random() * list.length)]!;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Built-in generator: same shape as the dictionary one, from a 16 x 16 pool. */
|
|
48
|
+
export const fallbackIdentityGenerator: IdentityGenerator = () =>
|
|
49
|
+
`${pick(FALLBACK_ADJECTIVES)}-${pick(FALLBACK_NOUNS)}-tater`;
|
|
50
|
+
|
|
51
|
+
let generator: IdentityGenerator = fallbackIdentityGenerator;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Register the generator `generateIdentity()` delegates to. Must return a
|
|
55
|
+
* string synchronously. `./identity-tater` registers the full dictionary;
|
|
56
|
+
* a host may register its own via `configurePlannotatorUI({ identityGenerator })`.
|
|
57
|
+
*/
|
|
58
|
+
export function setIdentityGenerator(next: IdentityGenerator): void {
|
|
59
|
+
generator = next;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The active generator. Exported so a test can assert which one is registered. */
|
|
63
|
+
export function getIdentityGenerator(): IdentityGenerator {
|
|
64
|
+
return generator;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Reset to the built-in fallback pool. Mainly for tests. */
|
|
68
|
+
export function resetIdentityGenerator(): void {
|
|
69
|
+
generator = fallbackIdentityGenerator;
|
|
70
|
+
}
|
|
10
71
|
|
|
11
72
|
/**
|
|
12
73
|
* Generate a new random tater identity.
|
|
@@ -14,16 +75,5 @@ import { uniqueUsernameGenerator, adjectives, nouns } from 'unique-username-gene
|
|
|
14
75
|
* Examples: "swift-falcon-tater", "gentle-crystal-tater"
|
|
15
76
|
*/
|
|
16
77
|
export function generateIdentity(): string {
|
|
17
|
-
|
|
18
|
-
// with compound words that contain hyphens (e.g., "behind-the-scenes")
|
|
19
|
-
const generated = uniqueUsernameGenerator({
|
|
20
|
-
dictionaries: [adjectives, nouns],
|
|
21
|
-
separator: '|||',
|
|
22
|
-
style: 'lowerCase',
|
|
23
|
-
randomDigits: 0,
|
|
24
|
-
length: 50, // Prevent word truncation (default is too short)
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
const [adjective, noun] = generated.split('|||');
|
|
28
|
-
return `${adjective}-${noun}-tater`;
|
|
78
|
+
return generator();
|
|
29
79
|
}
|
package/utils/htmlChrome.ts
CHANGED
|
@@ -2,37 +2,41 @@ import { storage } from './storage';
|
|
|
2
2
|
import { isStalePreference } from './preferenceTtl';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Cross-session
|
|
5
|
+
* Cross-session sidebar/panel state for raw-HTML annotate sessions.
|
|
6
6
|
*
|
|
7
|
-
* A raw-HTML session
|
|
8
|
-
* the
|
|
9
|
-
* drawer
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* changes or annotation activity re-stamp it) expires back to the minimal
|
|
7
|
+
* A raw-HTML session opens with both side surfaces closed so the page gets
|
|
8
|
+
* the viewport; an explicit change the user makes (opening the sidebar or the
|
|
9
|
+
* annotations drawer) persists for later HTML sessions, but only while they
|
|
10
|
+
* keep using HTML annotate: state not refreshed within the staleness TTL
|
|
11
|
+
* (explicit changes or annotation activity re-stamp it) expires back to the
|
|
13
12
|
* defaults. Persisted as a cookie (like every other cross-session UI pref;
|
|
14
13
|
* hook servers run on random ports, and cookies are scoped by domain, not
|
|
15
14
|
* port). Markdown sessions are untouched. A legacy record without a timestamp
|
|
16
|
-
* has an unknowable age and is treated as expired
|
|
17
|
-
*
|
|
15
|
+
* has an unknowable age and is treated as expired.
|
|
16
|
+
*
|
|
17
|
+
* `toolsHidden` is the header "Hide tools" toggle: while true, ALL floating
|
|
18
|
+
* chrome over the page (sidebar tongue tabs + the comment/attachments
|
|
19
|
+
* cluster) is removed from the DOM. Restoring it hidden can never strand a
|
|
20
|
+
* user: the header button that flips it back is part of the header, not the
|
|
21
|
+
* hidden chrome.
|
|
18
22
|
*/
|
|
19
23
|
|
|
20
24
|
const STORAGE_KEY = 'plannotator-html-chrome';
|
|
21
25
|
|
|
22
26
|
export interface HtmlChromeState {
|
|
23
|
-
/** The header "Hide tools" toggle — true hides all annotation chrome. */
|
|
24
|
-
toolsHidden: boolean;
|
|
25
27
|
/** Whether the left sidebar was open when the user last left. */
|
|
26
28
|
sidebarOpen: boolean;
|
|
27
29
|
/** Whether the right annotations drawer was open when the user last left. */
|
|
28
30
|
panelOpen: boolean;
|
|
31
|
+
/** Whether ALL floating tools over the page were hidden when the user left. */
|
|
32
|
+
toolsHidden: boolean;
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
/** Default:
|
|
35
|
+
/** Default: both side surfaces closed — the page gets the viewport. */
|
|
32
36
|
export const DEFAULT_HTML_CHROME_STATE: HtmlChromeState = {
|
|
33
|
-
toolsHidden: true,
|
|
34
37
|
sidebarOpen: false,
|
|
35
38
|
panelOpen: false,
|
|
39
|
+
toolsHidden: false,
|
|
36
40
|
};
|
|
37
41
|
|
|
38
42
|
/** Pure resolution logic (exported for tests): raw cookie value → state. */
|
|
@@ -49,15 +53,15 @@ export function resolveHtmlChromeState(
|
|
|
49
53
|
const record = parsed as Record<string, unknown>;
|
|
50
54
|
if (isStalePreference(record.savedAt, now)) return DEFAULT_HTML_CHROME_STATE;
|
|
51
55
|
return {
|
|
52
|
-
toolsHidden: typeof record.toolsHidden === 'boolean'
|
|
53
|
-
? record.toolsHidden
|
|
54
|
-
: DEFAULT_HTML_CHROME_STATE.toolsHidden,
|
|
55
56
|
sidebarOpen: typeof record.sidebarOpen === 'boolean'
|
|
56
57
|
? record.sidebarOpen
|
|
57
58
|
: DEFAULT_HTML_CHROME_STATE.sidebarOpen,
|
|
58
59
|
panelOpen: typeof record.panelOpen === 'boolean'
|
|
59
60
|
? record.panelOpen
|
|
60
61
|
: DEFAULT_HTML_CHROME_STATE.panelOpen,
|
|
62
|
+
toolsHidden: typeof record.toolsHidden === 'boolean'
|
|
63
|
+
? record.toolsHidden
|
|
64
|
+
: DEFAULT_HTML_CHROME_STATE.toolsHidden,
|
|
61
65
|
};
|
|
62
66
|
} catch {
|
|
63
67
|
return DEFAULT_HTML_CHROME_STATE;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Eager identity registration: installs the full `unique-username-generator`
|
|
3
|
+
* dictionary into the generator slot in `./generateIdentity` at module
|
|
4
|
+
* evaluation, before any settings read can mint a name.
|
|
5
|
+
*
|
|
6
|
+
* Every Plannotator entry (`packages/editor/App.tsx`, `packages/review-editor/App.tsx`)
|
|
7
|
+
* imports this module for its side effect, which keeps Plannotator's tater
|
|
8
|
+
* names byte-identical to before: same library, same config, same call. A host
|
|
9
|
+
* that wants the full dictionary without its own identity provider imports it
|
|
10
|
+
* too:
|
|
11
|
+
*
|
|
12
|
+
* import '@plannotator/ui/utils/identity-tater';
|
|
13
|
+
*
|
|
14
|
+
* A host that provides `identityProvider` never calls the generator and should
|
|
15
|
+
* NOT import this, so the word lists stay out of its bundle.
|
|
16
|
+
*/
|
|
17
|
+
import { uniqueUsernameGenerator, adjectives, nouns } from 'unique-username-generator';
|
|
18
|
+
import { setIdentityGenerator, type IdentityGenerator } from './generateIdentity';
|
|
19
|
+
|
|
20
|
+
/** The dictionary generator Plannotator has always used. */
|
|
21
|
+
export const generateTaterIdentity: IdentityGenerator = () => {
|
|
22
|
+
// Use a unique separator to split adjective from noun, avoiding issues
|
|
23
|
+
// with compound words that contain hyphens (e.g., "behind-the-scenes")
|
|
24
|
+
const generated = uniqueUsernameGenerator({
|
|
25
|
+
dictionaries: [adjectives, nouns],
|
|
26
|
+
separator: '|||',
|
|
27
|
+
style: 'lowerCase',
|
|
28
|
+
randomDigits: 0,
|
|
29
|
+
length: 50, // Prevent word truncation (default is too short)
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const [adjective, noun] = generated.split('|||');
|
|
33
|
+
return `${adjective}-${noun}-tater`;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
setIdentityGenerator(generateTaterIdentity);
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tracks whether the user has
|
|
3
|
-
*
|
|
2
|
+
* Tracks whether the user has explicitly resolved the Grid/Clean plan choice.
|
|
3
|
+
* This is separate from `gridEnabled`: ConfigStore seeds that preference with
|
|
4
|
+
* its default on first access, which is not evidence that the user chose it.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import { storage } from './storage';
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
const CURRENT_VERSION = '2';
|
|
9
|
+
const CHOICE_RESOLVED_KEY = 'plannotator-plan-look-choice-resolved';
|
|
10
|
+
const LEGACY_ANNOUNCEMENT_KEY = 'plannotator-look-feel-announcement-seen';
|
|
11
11
|
|
|
12
12
|
export function needsLookAndFeelAnnouncement(): boolean {
|
|
13
|
-
|
|
13
|
+
if (storage.getItem(CHOICE_RESOLVED_KEY) === 'true') return false;
|
|
14
|
+
// v2 was the old Grid/Clean chooser wrapped in the 0.20.0 announcement.
|
|
15
|
+
// A dismissal there resolved the same decision; migrate it without showing
|
|
16
|
+
// another dialog merely because the release framing was removed.
|
|
17
|
+
return storage.getItem(LEGACY_ANNOUNCEMENT_KEY) !== '2';
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
export function
|
|
17
|
-
storage.setItem(
|
|
20
|
+
export function markLookAndFeelChoiceResolved(): void {
|
|
21
|
+
storage.setItem(CHOICE_RESOLVED_KEY, 'true');
|
|
18
22
|
}
|