@plannotator/ui 0.29.1 → 0.31.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 +1 -0
- package/components/ActionMenu.tsx +6 -1
- package/components/AgentsTab.tsx +8 -9
- package/components/AnalysisLayerToggle.tsx +48 -0
- package/components/AnnotationPanel.tsx +68 -35
- package/components/AnnotationToolbar.tsx +28 -8
- package/components/AnnotationToolstrip.tsx +9 -0
- package/components/CommentPopover.tsx +212 -46
- package/components/ConfirmDialog.tsx +42 -28
- package/components/InlineMarkdown.tsx +3 -2
- 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/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 +136 -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/core/button.tsx +14 -6
- package/components/html-viewer/HtmlViewer.tsx +215 -39
- package/components/html-viewer/bridge-script.ts +384 -66
- package/components/html-viewer/composerYield.ts +1 -51
- package/components/html-viewer/useHtmlAnnotation.ts +179 -58
- 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/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/useIsMobile.ts +37 -0
- package/hooks/useLinkedDoc.ts +7 -0
- package/hooks/useScrollViewport.ts +74 -0
- package/hooks/useViewportEnvironment.ts +350 -0
- package/package.json +3 -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 +34 -0
- package/utils/annotateAgentTerminal.ts +36 -5
- package/utils/blockTargeting.ts +6 -3
- package/utils/composerYield.ts +45 -0
- package/utils/htmlChrome.ts +20 -16
- package/utils/lookAndFeelAnnouncement.ts +12 -8
- package/utils/markdownExtensions.ts +57 -0
- package/utils/parser.ts +37 -2
- package/utils/vimNavigation.ts +4 -1
- package/utils/vimScroll.ts +9 -4
- package/utils/wideMode.ts +20 -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,7 @@ 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
|
|
82
83
|
htmlAnchor?: HtmlElementAnchor; // raw-HTML pinpoint: serialized element anchor for reliable restoration
|
|
83
84
|
htmlAdditionalTargets?: HtmlAnnotationTarget[]; // raw-HTML shift-click multi-select: extra elements this one comment covers (primary stays htmlAnchor/originalText)
|
|
84
85
|
// web-highlighter metadata for cross-element selections
|
|
@@ -155,6 +156,32 @@ export type CodeAnnotationType = 'comment' | 'suggestion' | 'concern';
|
|
|
155
156
|
// must branch on scope, never read those sentinels as a real path or row.
|
|
156
157
|
export type CodeAnnotationScope = 'line' | 'file' | 'general';
|
|
157
158
|
|
|
159
|
+
/**
|
|
160
|
+
* One inferred step selected from the Call Flow analysis surface.
|
|
161
|
+
*
|
|
162
|
+
* Source location is optional because CallDiff can surface structural steps
|
|
163
|
+
* without a concrete line. `CodeAnnotation` uses an in-patch located target
|
|
164
|
+
* as its native inline anchor when one exists; otherwise the annotation is
|
|
165
|
+
* file- or review-scoped while this target remains its durable Call Flow
|
|
166
|
+
* anchor. Raw output selections use a one-based `rawLine` instead of a source
|
|
167
|
+
* location. This lets every rendered Call Flow row and raw line participate in
|
|
168
|
+
* feedback without pretending an out-of-hunk or diagnostic line can be posted
|
|
169
|
+
* as an inline source comment.
|
|
170
|
+
*/
|
|
171
|
+
interface CallFlowAnnotationTargetBase {
|
|
172
|
+
treePath: string;
|
|
173
|
+
entry: string;
|
|
174
|
+
label: string;
|
|
175
|
+
side: 'old' | 'new';
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type CallFlowAnnotationTarget = CallFlowAnnotationTargetBase & (
|
|
179
|
+
| { filePath: string; lineStart: number; lineEnd: number; rawLine?: undefined }
|
|
180
|
+
| { filePath: string; lineStart?: undefined; lineEnd?: undefined; rawLine?: undefined }
|
|
181
|
+
| { rawLine: number; filePath?: undefined; lineStart?: undefined; lineEnd?: undefined }
|
|
182
|
+
| { rawLine?: undefined; filePath?: undefined; lineStart?: undefined; lineEnd?: undefined }
|
|
183
|
+
);
|
|
184
|
+
|
|
158
185
|
/** Conventional Comments label — see https://conventionalcomments.org */
|
|
159
186
|
export type ConventionalLabel =
|
|
160
187
|
| 'praise'
|
|
@@ -214,6 +241,13 @@ export interface CodeAnnotation {
|
|
|
214
241
|
* line anchor maps to the pristine lines those edits replace, so it is
|
|
215
242
|
* approximate and the export labels it as such. */
|
|
216
243
|
selectedTextFromEdits?: boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Complete Call Flow selection for an annotation authored from that
|
|
246
|
+
* surface. When any target maps to the patch, one target also supplies this
|
|
247
|
+
* annotation's primary inline anchor; otherwise the annotation is file- or
|
|
248
|
+
* review-scoped. Target order always preserves the user's selection order.
|
|
249
|
+
*/
|
|
250
|
+
callFlowTargets?: CallFlowAnnotationTarget[];
|
|
217
251
|
createdAt: number;
|
|
218
252
|
author?: string;
|
|
219
253
|
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
|
+
}
|
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;
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extra markdown extensions, renderer side (#1307).
|
|
3
|
+
*
|
|
4
|
+
* The server resolves `markdownExtensions` from `~/.plannotator/config.json`
|
|
5
|
+
* and ships the normalized list with the annotate payload. The renderer needs
|
|
6
|
+
* it for one job: deciding whether a relative link or a wiki-link target names
|
|
7
|
+
* a local document it should open in the linked-doc overlay (`/api/doc`) or a
|
|
8
|
+
* plain external link. Without it, `[notes](notes.livemd)` renders as a dead
|
|
9
|
+
* external link even though the server would happily serve it.
|
|
10
|
+
*
|
|
11
|
+
* Module-level registry seam, like `skillReferences.ts`: a host (or the app's
|
|
12
|
+
* own boot code) registers the list once, everything else reads it. Empty by
|
|
13
|
+
* default, so nothing changes for a user with no config.
|
|
14
|
+
*
|
|
15
|
+
* The built-in set here is deliberately NARROWER than the annotatable set on
|
|
16
|
+
* the server (`.md`/`.mdx`/`.txt`/`.html`/`.htm` only) — widening it is a
|
|
17
|
+
* separate decision. Extras are added on top of it.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { normalizeMarkdownExtensions } from "@plannotator/core/annotatable";
|
|
21
|
+
|
|
22
|
+
/** Built-in extensions the renderer treats as openable local documents. */
|
|
23
|
+
const BUILTIN_LINKED_DOC_REGEX = /\.(mdx?|txt|html?)$/i;
|
|
24
|
+
|
|
25
|
+
let extraExtensions: string[] = [];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register the extra markdown extensions for this page. Values are normalized
|
|
29
|
+
* with the same rules the server applies (dot-led, lowercased, `.env` denied),
|
|
30
|
+
* so a hostile or malformed payload cannot inject regex or path fragments.
|
|
31
|
+
*/
|
|
32
|
+
export function setExtraMarkdownExtensions(value: unknown): void {
|
|
33
|
+
extraExtensions = normalizeMarkdownExtensions(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The registered extra extensions (normalized, possibly empty). */
|
|
37
|
+
export function getExtraMarkdownExtensions(): string[] {
|
|
38
|
+
return extraExtensions;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Does this link target name a local document the linked-doc overlay can open?
|
|
43
|
+
*
|
|
44
|
+
* `allowFragment` mirrors the two call sites this replaced: markdown links
|
|
45
|
+
* accept a trailing `#fragment` (stripped by the caller before navigating),
|
|
46
|
+
* wiki-link targets do not.
|
|
47
|
+
*/
|
|
48
|
+
export function hasLinkedDocExtension(
|
|
49
|
+
target: string,
|
|
50
|
+
options?: { allowFragment?: boolean },
|
|
51
|
+
): boolean {
|
|
52
|
+
const trimmed = target.trim();
|
|
53
|
+
const path = options?.allowFragment ? trimmed.replace(/#.*$/, "") : trimmed;
|
|
54
|
+
if (BUILTIN_LINKED_DOC_REGEX.test(path)) return true;
|
|
55
|
+
const lower = path.toLowerCase();
|
|
56
|
+
return extraExtensions.some((ext) => lower.endsWith(ext));
|
|
57
|
+
}
|
package/utils/parser.ts
CHANGED
|
@@ -1178,8 +1178,43 @@ export const exportAnnotations = (
|
|
|
1178
1178
|
output += `I've reviewed this ${subject} and have ${annotations.length} piece${annotations.length > 1 ? 's' : ''} of feedback:\n\n`;
|
|
1179
1179
|
}
|
|
1180
1180
|
|
|
1181
|
-
|
|
1182
|
-
|
|
1181
|
+
// Live app sessions stamp annotations with the page they were made on.
|
|
1182
|
+
// When any exported annotation carries a pageUrl, entries are grouped under
|
|
1183
|
+
// per-page `## Page:` headings in order of first appearance and every entry
|
|
1184
|
+
// demotes to `###` so it nests BELOW its page header (a `### Page:` header
|
|
1185
|
+
// over `##` entries would invert the hierarchy); annotations without a page
|
|
1186
|
+
// (e.g. globals) come first under no heading, at the same `###` level so
|
|
1187
|
+
// entries render uniformly. Numbers stay GLOBAL: each entry keeps the
|
|
1188
|
+
// number of its position in the ungrouped order, matching the on-page
|
|
1189
|
+
// marker numbering, so grouped sections may show non-contiguous numbers.
|
|
1190
|
+
// With no pageUrl anywhere the output is byte-identical to the ungrouped
|
|
1191
|
+
// export (`## N.` entries, no page headers).
|
|
1192
|
+
const hasPageGroups = sortedAnns.some(
|
|
1193
|
+
(a: any) => typeof a.pageUrl === 'string' && a.pageUrl.length > 0,
|
|
1194
|
+
);
|
|
1195
|
+
const annotationNumbers = new Map<any, number>(
|
|
1196
|
+
sortedAnns.map((ann, index) => [ann, index + 1]),
|
|
1197
|
+
);
|
|
1198
|
+
let emitOrder = sortedAnns;
|
|
1199
|
+
if (hasPageGroups) {
|
|
1200
|
+
const unpaged = sortedAnns.filter((a: any) => !a.pageUrl);
|
|
1201
|
+
const pageOrder: string[] = [];
|
|
1202
|
+
for (const ann of sortedAnns) {
|
|
1203
|
+
if (ann.pageUrl && !pageOrder.includes(ann.pageUrl)) pageOrder.push(ann.pageUrl);
|
|
1204
|
+
}
|
|
1205
|
+
emitOrder = [
|
|
1206
|
+
...unpaged,
|
|
1207
|
+
...pageOrder.flatMap((page) => sortedAnns.filter((a: any) => a.pageUrl === page)),
|
|
1208
|
+
];
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
let lastEmittedPage: string | null = null;
|
|
1212
|
+
emitOrder.forEach((ann) => {
|
|
1213
|
+
if (hasPageGroups && ann.pageUrl && ann.pageUrl !== lastEmittedPage) {
|
|
1214
|
+
output += `## Page: ${ann.pageUrl}\n\n`;
|
|
1215
|
+
lastEmittedPage = ann.pageUrl;
|
|
1216
|
+
}
|
|
1217
|
+
output += `${hasPageGroups ? '###' : '##'} ${annotationNumbers.get(ann)}. `;
|
|
1183
1218
|
|
|
1184
1219
|
// Add diff context label if annotation was created in diff view
|
|
1185
1220
|
if (ann.diffContext) {
|
package/utils/vimNavigation.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getScrollViewportRect } from '../hooks/useScrollViewport';
|
|
1
2
|
import { getAnnotatableTextNodes } from './domSelection';
|
|
2
3
|
|
|
3
4
|
/** A durable cursor location measured within one rendered Markdown block. */
|
|
@@ -132,7 +133,9 @@ function getViewportCenterY(
|
|
|
132
133
|
container: HTMLElement,
|
|
133
134
|
scrollViewport?: HTMLElement | null,
|
|
134
135
|
): number {
|
|
135
|
-
const rect =
|
|
136
|
+
const rect = scrollViewport
|
|
137
|
+
? getScrollViewportRect(scrollViewport)
|
|
138
|
+
: container.getBoundingClientRect();
|
|
136
139
|
return rect.top + rect.height / 2;
|
|
137
140
|
}
|
|
138
141
|
|