@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
|
@@ -20,6 +20,9 @@ import { OverlayScrollArea } from "../OverlayScrollArea";
|
|
|
20
20
|
import { ReviewAgentsIcon } from "../ReviewAgentsIcon";
|
|
21
21
|
|
|
22
22
|
interface SidebarContainerProps {
|
|
23
|
+
/** Desktop preserves the incumbent sticky rail. Compact Plan uses the same
|
|
24
|
+
* content as a safe, visible-viewport-bounded foreground surface. */
|
|
25
|
+
presentation?: "desktop" | "overlay";
|
|
23
26
|
activeTab: SidebarTab;
|
|
24
27
|
onTabChange: (tab: SidebarTab) => void;
|
|
25
28
|
onClose: () => void;
|
|
@@ -29,6 +32,7 @@ interface SidebarContainerProps {
|
|
|
29
32
|
isAgentTerminalRunning?: boolean;
|
|
30
33
|
onToggleAgentTerminal?: () => void;
|
|
31
34
|
// TOC props
|
|
35
|
+
showContentsTab?: boolean;
|
|
32
36
|
blocks: Block[];
|
|
33
37
|
annotations: Annotation[];
|
|
34
38
|
activeSection: string | null;
|
|
@@ -45,6 +49,8 @@ interface SidebarContainerProps {
|
|
|
45
49
|
onFilesSelectFile?: (absolutePath: string, dirPath: string) => void;
|
|
46
50
|
onFilesFetchAll?: () => void;
|
|
47
51
|
onFilesRetryVaultDir?: (vaultPath: string) => void;
|
|
52
|
+
/** Compact-only file activation feedback; desktop does not pass this. */
|
|
53
|
+
pendingFileLabel?: string | null;
|
|
48
54
|
// Version Browser props
|
|
49
55
|
showVersionsTab?: boolean;
|
|
50
56
|
versionInfo: VersionInfo | null;
|
|
@@ -74,6 +80,7 @@ interface SidebarContainerProps {
|
|
|
74
80
|
}
|
|
75
81
|
|
|
76
82
|
export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
83
|
+
presentation = "desktop",
|
|
77
84
|
activeTab,
|
|
78
85
|
onTabChange,
|
|
79
86
|
onClose,
|
|
@@ -82,6 +89,7 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
82
89
|
isAgentTerminalOpen,
|
|
83
90
|
isAgentTerminalRunning,
|
|
84
91
|
onToggleAgentTerminal,
|
|
92
|
+
showContentsTab = true,
|
|
85
93
|
blocks,
|
|
86
94
|
annotations,
|
|
87
95
|
activeSection,
|
|
@@ -97,6 +105,7 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
97
105
|
onFilesSelectFile,
|
|
98
106
|
onFilesFetchAll,
|
|
99
107
|
onFilesRetryVaultDir,
|
|
108
|
+
pendingFileLabel,
|
|
100
109
|
showVersionsTab,
|
|
101
110
|
versionInfo,
|
|
102
111
|
versions,
|
|
@@ -121,14 +130,87 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
121
130
|
onSelectMessage,
|
|
122
131
|
messageAnnotationCounts,
|
|
123
132
|
}) => {
|
|
133
|
+
const compact = presentation === "overlay";
|
|
134
|
+
const closeButtonRef = React.useRef<HTMLButtonElement>(null);
|
|
135
|
+
|
|
136
|
+
React.useEffect(() => {
|
|
137
|
+
if (compact) closeButtonRef.current?.focus({ preventScroll: true });
|
|
138
|
+
}, [compact]);
|
|
139
|
+
|
|
140
|
+
const handleCompactKeyDown = (event: React.KeyboardEvent<HTMLElement>) => {
|
|
141
|
+
if (!compact) return;
|
|
142
|
+
if (event.key === "Escape") {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
event.stopPropagation();
|
|
145
|
+
onClose();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (event.key !== "Tab") return;
|
|
149
|
+
const focusable = Array.from(
|
|
150
|
+
event.currentTarget.querySelectorAll<HTMLElement>(
|
|
151
|
+
'button:not(:disabled), input:not(:disabled), [href], [tabindex]:not([tabindex="-1"])',
|
|
152
|
+
),
|
|
153
|
+
);
|
|
154
|
+
if (focusable.length === 0) return;
|
|
155
|
+
const first = focusable[0];
|
|
156
|
+
const last = focusable[focusable.length - 1];
|
|
157
|
+
if (event.shiftKey && document.activeElement === first) {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
last?.focus();
|
|
160
|
+
} else if (!event.shiftKey && document.activeElement === last) {
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
first?.focus();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
124
166
|
return (
|
|
125
167
|
<aside
|
|
126
|
-
|
|
127
|
-
|
|
168
|
+
id={compact ? "pn-compact-plan-navigator" : undefined}
|
|
169
|
+
data-pn-plan-navigator={compact ? "true" : undefined}
|
|
170
|
+
// The compact navigator (Contents / Versions / Archive) is the same kind
|
|
171
|
+
// of full-viewport transient surface as CompactPlanStage, so it must not
|
|
172
|
+
// print over the document either. Desktop rail printing is unchanged.
|
|
173
|
+
data-print-hide={compact ? true : undefined}
|
|
174
|
+
role={compact ? "dialog" : undefined}
|
|
175
|
+
aria-modal={compact ? true : undefined}
|
|
176
|
+
aria-label={compact ? "Plan navigator" : undefined}
|
|
177
|
+
onKeyDown={compact ? handleCompactKeyDown : undefined}
|
|
178
|
+
className={compact
|
|
179
|
+
? "pn-visible-viewport-stage z-[90] flex flex-col overflow-hidden bg-card text-foreground"
|
|
180
|
+
: "hidden lg:flex flex-col sticky top-12 h-[calc(100vh-3rem)] flex-shrink-0 bg-card border-r border-border"}
|
|
181
|
+
style={{ width: compact ? undefined : width }}
|
|
128
182
|
>
|
|
183
|
+
{compact && (
|
|
184
|
+
<div className="flex h-[52px] flex-shrink-0 items-center justify-between border-b border-border/50 px-3">
|
|
185
|
+
<div className="min-w-0">
|
|
186
|
+
<p className="text-sm font-semibold tracking-tight">Navigate</p>
|
|
187
|
+
<p className="truncate text-[11px] text-muted-foreground" aria-live="polite">
|
|
188
|
+
{pendingFileLabel ? `Opening ${pendingFileLabel}…` : 'Choose what to review'}
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
<button
|
|
192
|
+
ref={closeButtonRef}
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={onClose}
|
|
195
|
+
data-pn-touch-target="true"
|
|
196
|
+
data-pn-touch-target-icon="true"
|
|
197
|
+
className="flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/60"
|
|
198
|
+
aria-label="Close navigator"
|
|
199
|
+
title="Close navigator"
|
|
200
|
+
>
|
|
201
|
+
<svg className="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
|
|
202
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
203
|
+
</svg>
|
|
204
|
+
</button>
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
129
208
|
{/* Tab bar */}
|
|
130
|
-
<div className=
|
|
131
|
-
|
|
209
|
+
<div className={compact
|
|
210
|
+
? "flex min-h-11 items-center gap-1 overflow-x-auto border-b border-border/50 px-2 py-1"
|
|
211
|
+
: "flex h-10 items-center border-b border-border/50 px-2 gap-0.5 flex-shrink-0 overflow-hidden min-w-0"}
|
|
212
|
+
>
|
|
213
|
+
{!compact && showAgentTerminalButton && onToggleAgentTerminal && (
|
|
132
214
|
<ActionButton
|
|
133
215
|
active={!!isAgentTerminalOpen}
|
|
134
216
|
running={!!isAgentTerminalRunning}
|
|
@@ -137,26 +219,29 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
137
219
|
label="Agent"
|
|
138
220
|
/>
|
|
139
221
|
)}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
222
|
+
{showContentsTab && (
|
|
223
|
+
<TabButton
|
|
224
|
+
active={activeTab === "toc"}
|
|
225
|
+
onClick={() => onTabChange("toc")}
|
|
226
|
+
icon={
|
|
227
|
+
<svg
|
|
228
|
+
className="w-3 h-3"
|
|
229
|
+
fill="none"
|
|
230
|
+
viewBox="0 0 24 24"
|
|
231
|
+
stroke="currentColor"
|
|
232
|
+
strokeWidth={2}
|
|
233
|
+
>
|
|
234
|
+
<path
|
|
235
|
+
strokeLinecap="round"
|
|
236
|
+
strokeLinejoin="round"
|
|
237
|
+
d="M4 6h16M4 10h16M4 14h10M4 18h10"
|
|
238
|
+
/>
|
|
239
|
+
</svg>
|
|
240
|
+
}
|
|
241
|
+
label="Contents"
|
|
242
|
+
touch={compact}
|
|
243
|
+
/>
|
|
244
|
+
)}
|
|
160
245
|
{showVersionsTab && (
|
|
161
246
|
<TabButton
|
|
162
247
|
active={activeTab === "versions"}
|
|
@@ -177,6 +262,7 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
177
262
|
</svg>
|
|
178
263
|
}
|
|
179
264
|
label="Versions"
|
|
265
|
+
touch={compact}
|
|
180
266
|
/>
|
|
181
267
|
)}
|
|
182
268
|
{showMessagesTab && (
|
|
@@ -186,6 +272,7 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
186
272
|
icon={<MessagesIcon className="w-3 h-3" />}
|
|
187
273
|
label="Messages"
|
|
188
274
|
badge={messageAnnotationCounts !== undefined && messageAnnotationCounts.size > 0}
|
|
275
|
+
touch={compact}
|
|
189
276
|
/>
|
|
190
277
|
)}
|
|
191
278
|
{showFilesTab && (
|
|
@@ -209,6 +296,7 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
209
296
|
}
|
|
210
297
|
label="Files"
|
|
211
298
|
badge={hasFileAnnotations}
|
|
299
|
+
touch={compact}
|
|
212
300
|
/>
|
|
213
301
|
)}
|
|
214
302
|
{showArchiveTab && (
|
|
@@ -231,6 +319,7 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
231
319
|
</svg>
|
|
232
320
|
}
|
|
233
321
|
label="Archive"
|
|
322
|
+
touch={compact}
|
|
234
323
|
/>
|
|
235
324
|
)}
|
|
236
325
|
{/* No header close button — the sidebar collapses via the resize-handle
|
|
@@ -238,8 +327,11 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
238
327
|
</div>
|
|
239
328
|
|
|
240
329
|
{/* Content area */}
|
|
241
|
-
<OverlayScrollArea className=
|
|
242
|
-
|
|
330
|
+
<OverlayScrollArea className={compact
|
|
331
|
+
? "flex-1 min-h-0 overscroll-contain [-webkit-overflow-scrolling:touch]"
|
|
332
|
+
: "flex-1 min-h-0"}
|
|
333
|
+
>
|
|
334
|
+
{activeTab === "toc" && showContentsTab && (
|
|
243
335
|
<TableOfContents
|
|
244
336
|
blocks={blocks}
|
|
245
337
|
annotations={annotations}
|
|
@@ -280,6 +372,7 @@ export const SidebarContainer: React.FC<SidebarContainerProps> = ({
|
|
|
280
372
|
annotationCounts={fileAnnotationCounts}
|
|
281
373
|
highlightedFiles={highlightedFiles}
|
|
282
374
|
editStatuses={fileEditStatuses}
|
|
375
|
+
selectionPending={compact && !!pendingFileLabel}
|
|
283
376
|
/>
|
|
284
377
|
)}
|
|
285
378
|
{activeTab === "archive" && showArchiveTab && (
|
|
@@ -309,10 +402,13 @@ const TabButton: React.FC<{
|
|
|
309
402
|
icon: React.ReactNode;
|
|
310
403
|
label: string;
|
|
311
404
|
badge?: boolean;
|
|
312
|
-
|
|
405
|
+
touch?: boolean;
|
|
406
|
+
}> = ({ active, onClick, icon, label, badge, touch = false }) => (
|
|
313
407
|
<button
|
|
408
|
+
type="button"
|
|
314
409
|
onClick={onClick}
|
|
315
|
-
|
|
410
|
+
data-pn-touch-target={touch ? "true" : undefined}
|
|
411
|
+
className={`relative flex items-center gap-1 px-2 py-1 rounded text-xs font-medium transition-colors min-w-0 shrink-0${touch ? " h-9" : ""} ${
|
|
316
412
|
active
|
|
317
413
|
? "bg-primary/10 text-primary"
|
|
318
414
|
: "text-muted-foreground hover:text-foreground hover:bg-muted/50"
|
package/components/ui/button.tsx
CHANGED
|
@@ -18,15 +18,15 @@ const buttonVariants = cva(
|
|
|
18
18
|
{
|
|
19
19
|
variants: {
|
|
20
20
|
variant: {
|
|
21
|
-
default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
|
|
21
|
+
default: "bg-primary text-primary-foreground shadow-xs [@media(hover:hover)_and_(pointer:fine)]:hover:bg-primary/90",
|
|
22
22
|
destructive:
|
|
23
|
-
"bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
|
24
|
-
success: "bg-success text-success-foreground shadow-xs hover:bg-success/90",
|
|
23
|
+
"bg-destructive text-white shadow-xs [@media(hover:hover)_and_(pointer:fine)]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40",
|
|
24
|
+
success: "bg-success text-success-foreground shadow-xs [@media(hover:hover)_and_(pointer:fine)]:hover:bg-success/90",
|
|
25
25
|
outline:
|
|
26
|
-
"border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
|
|
27
|
-
secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
|
|
28
|
-
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
29
|
-
link: "text-primary underline-offset-4 hover:underline",
|
|
26
|
+
"border border-input bg-background shadow-xs [@media(hover:hover)_and_(pointer:fine)]:hover:bg-accent [@media(hover:hover)_and_(pointer:fine)]:hover:text-accent-foreground",
|
|
27
|
+
secondary: "bg-secondary text-secondary-foreground shadow-xs [@media(hover:hover)_and_(pointer:fine)]:hover:bg-secondary/80",
|
|
28
|
+
ghost: "[@media(hover:hover)_and_(pointer:fine)]:hover:bg-accent [@media(hover:hover)_and_(pointer:fine)]:hover:text-accent-foreground",
|
|
29
|
+
link: "text-primary underline-offset-4 [@media(hover:hover)_and_(pointer:fine)]:hover:underline",
|
|
30
30
|
},
|
|
31
31
|
size: {
|
|
32
32
|
default: "h-9 px-4 py-2",
|
|
@@ -61,9 +61,11 @@ function Button({
|
|
|
61
61
|
}) {
|
|
62
62
|
return (
|
|
63
63
|
<ButtonPrimitive
|
|
64
|
+
{...props}
|
|
64
65
|
data-slot="button"
|
|
66
|
+
data-pn-touch-target="true"
|
|
67
|
+
data-pn-touch-target-icon={size === "icon" ? "true" : undefined}
|
|
65
68
|
className={cn(buttonVariants({ variant, size, className }))}
|
|
66
|
-
{...props}
|
|
67
69
|
>
|
|
68
70
|
{iconLeft ? (
|
|
69
71
|
<span data-slot="button-icon" data-side="left" aria-hidden="true" className="shrink-0">
|
package/components/ui/dialog.tsx
CHANGED
|
@@ -14,6 +14,7 @@ const DialogOverlay = React.forwardRef<
|
|
|
14
14
|
>(({ className, ...props }, ref) => (
|
|
15
15
|
<DialogPrimitive.Backdrop
|
|
16
16
|
ref={ref}
|
|
17
|
+
data-slot="dialog-overlay"
|
|
17
18
|
className={cn(
|
|
18
19
|
"fixed inset-0 z-[110] bg-black/55 backdrop-blur-[2px]",
|
|
19
20
|
"transition-opacity duration-200",
|
|
@@ -28,34 +29,43 @@ DialogOverlay.displayName = "DialogOverlay";
|
|
|
28
29
|
const DialogContent = React.forwardRef<
|
|
29
30
|
React.ComponentRef<typeof DialogPrimitive.Popup>,
|
|
30
31
|
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Popup> & {
|
|
32
|
+
/** Optional visual override for the backdrop rendered with this content. */
|
|
33
|
+
backdropClassName?: string;
|
|
34
|
+
/** Removes the default top-right close control. */
|
|
31
35
|
hideClose?: boolean;
|
|
32
36
|
}
|
|
33
|
-
>(({ className, children, hideClose, ...props }, ref) => (
|
|
37
|
+
>(({ backdropClassName, className, children, hideClose, ...props }, ref) => (
|
|
34
38
|
<DialogPortal>
|
|
35
|
-
<DialogOverlay />
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
39
|
+
<DialogOverlay className={backdropClassName} />
|
|
40
|
+
<div className="pn-visible-viewport-overlay z-[110] pointer-events-none flex items-center justify-center">
|
|
41
|
+
<DialogPrimitive.Popup
|
|
42
|
+
ref={ref}
|
|
43
|
+
className={cn(
|
|
44
|
+
"relative pointer-events-auto z-[110] w-full min-h-0",
|
|
45
|
+
"max-w-4xl max-h-[min(640px,85vh,100%)]",
|
|
46
|
+
"flex flex-col overflow-hidden",
|
|
47
|
+
"rounded-2xl border border-border bg-popover text-popover-foreground",
|
|
48
|
+
"shadow-[0_24px_80px_-36px_rgba(15,23,42,0.5)]",
|
|
49
|
+
"transition-[opacity,scale] duration-200",
|
|
50
|
+
"data-starting-style:opacity-0 data-starting-style:scale-95",
|
|
51
|
+
"data-ending-style:opacity-0 data-ending-style:scale-95",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
{!hideClose && (
|
|
58
|
+
<DialogPrimitive.Close
|
|
59
|
+
data-pn-touch-target="true"
|
|
60
|
+
data-pn-touch-target-icon="true"
|
|
61
|
+
className="absolute top-3.5 right-3.5 inline-flex items-center justify-center rounded-md p-1.5 text-muted-foreground [@media(hover:hover)_and_(pointer:fine)]:hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring/50"
|
|
62
|
+
>
|
|
63
|
+
<X className="size-4" />
|
|
64
|
+
<span className="sr-only">Close</span>
|
|
65
|
+
</DialogPrimitive.Close>
|
|
66
|
+
)}
|
|
67
|
+
</DialogPrimitive.Popup>
|
|
68
|
+
</div>
|
|
59
69
|
</DialogPortal>
|
|
60
70
|
));
|
|
61
71
|
DialogContent.displayName = "DialogContent";
|
package/config/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export { configStore } from './configStore';
|
|
2
2
|
export type { ServerSyncFn } from './configStore';
|
|
3
3
|
export { useConfigValue } from './useConfig';
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
setReviewPanelView,
|
|
6
|
+
setReviewDefaultDiffType,
|
|
7
|
+
getPersistedReviewPanelView,
|
|
8
|
+
type ReviewDefaultDiffType,
|
|
9
|
+
} from './reviewView';
|
package/config/reviewView.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { configStore } from './configStore';
|
|
2
|
+
import { SETTINGS } from './settings';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* The ONLY writers for the coupled setting pair (reviewPanelView,
|
|
@@ -17,13 +18,39 @@ import { configStore } from './configStore';
|
|
|
17
18
|
* setting directly — always go through these setters. configStore.init()
|
|
18
19
|
* remains the one non-writer that can produce a conflicted pair from a
|
|
19
20
|
* stale config.json; the App-level load reconciler heals that case by
|
|
20
|
-
* calling setReviewPanelView('sections').
|
|
21
|
+
* calling setReviewPanelView('sections', { recordLastUsed: false }).
|
|
21
22
|
*/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
|
|
24
|
+
/** Store seam for tests (fresh ConfigStoreForTest); production always uses the singleton. */
|
|
25
|
+
type PanelViewConfigStore = typeof configStore;
|
|
26
|
+
|
|
27
|
+
export function setReviewPanelView(
|
|
28
|
+
view: 'sections' | 'tree',
|
|
29
|
+
options?: { recordLastUsed?: boolean },
|
|
30
|
+
store: PanelViewConfigStore = configStore,
|
|
31
|
+
): void {
|
|
32
|
+
store.set('reviewPanelView', view);
|
|
33
|
+
// An explicit persisted choice also becomes the last-used view — otherwise
|
|
34
|
+
// a stale last-used cookie would immediately shadow what the user just
|
|
35
|
+
// picked in Settings / the setup dialog. recordLastUsed: false is for
|
|
36
|
+
// NON-choices: the App self-heal repairs a conflicted persisted pair
|
|
37
|
+
// without any user action, so it must not overwrite the user's memo.
|
|
38
|
+
if (options?.recordLastUsed !== false) {
|
|
39
|
+
store.set('reviewPanelViewLastUsed', view);
|
|
26
40
|
}
|
|
41
|
+
if (view === 'sections' && store.get('defaultDiffType') !== 'since-base') {
|
|
42
|
+
store.set('defaultDiffType', 'since-base');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The panel view the reviewer has actually persisted, or `undefined` when they
|
|
48
|
+
* never chose one. Distinct from `configStore.get('reviewPanelView')`, which
|
|
49
|
+
* cannot tell a stored choice apart from the built-in default, which is the
|
|
50
|
+
* difference first-run seeding has to respect before it writes over anything.
|
|
51
|
+
*/
|
|
52
|
+
export function getPersistedReviewPanelView(): 'sections' | 'tree' | undefined {
|
|
53
|
+
return SETTINGS.reviewPanelView.fromCookie();
|
|
27
54
|
}
|
|
28
55
|
|
|
29
56
|
export type ReviewDefaultDiffType =
|
|
@@ -34,9 +61,15 @@ export type ReviewDefaultDiffType =
|
|
|
34
61
|
| 'merge-base'
|
|
35
62
|
| 'all';
|
|
36
63
|
|
|
37
|
-
export function setReviewDefaultDiffType(
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
export function setReviewDefaultDiffType(
|
|
65
|
+
value: ReviewDefaultDiffType,
|
|
66
|
+
store: PanelViewConfigStore = configStore,
|
|
67
|
+
): void {
|
|
68
|
+
store.set('defaultDiffType', value);
|
|
69
|
+
if (value !== 'since-base' && store.get('reviewPanelView') !== 'tree') {
|
|
70
|
+
store.set('reviewPanelView', 'tree');
|
|
71
|
+
// The snap is an explicit-choice consequence (the user picked a classic
|
|
72
|
+
// diff default), so it syncs the memo like any explicit view write.
|
|
73
|
+
store.set('reviewPanelViewLastUsed', 'tree');
|
|
41
74
|
}
|
|
42
75
|
}
|
package/config/settings.ts
CHANGED
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
* Add new settings here. Cookie-only settings omit serverKey.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import {
|
|
13
|
+
isAnnotateAgentTerminalSide,
|
|
14
|
+
type AnnotateAgentTerminalSide,
|
|
15
|
+
} from '@plannotator/core/agent-terminal';
|
|
12
16
|
import type { DiffLineBgIntensity } from '@plannotator/core/config-types';
|
|
17
|
+
import { isFaviconStyle, type FaviconStyle } from '@plannotator/core/favicon';
|
|
13
18
|
import { storage } from '../utils/storage';
|
|
14
19
|
import { generateIdentity } from '../utils/generateIdentity';
|
|
15
20
|
import {
|
|
@@ -131,6 +136,20 @@ export const SETTINGS = {
|
|
|
131
136
|
},
|
|
132
137
|
toServer: (v: ThemePair) => ({ theme: { mode: v.mode, light: v.light, dark: v.dark } }),
|
|
133
138
|
},
|
|
139
|
+
faviconStyle: {
|
|
140
|
+
defaultValue: 'totman' as FaviconStyle,
|
|
141
|
+
fromCookie: () => {
|
|
142
|
+
const v = storage.getItem('plannotator-favicon');
|
|
143
|
+
return isFaviconStyle(v) ? v : undefined;
|
|
144
|
+
},
|
|
145
|
+
toCookie: (v: FaviconStyle) => storage.setItem('plannotator-favicon', v),
|
|
146
|
+
serverKey: 'favicon',
|
|
147
|
+
fromServer: (sc: Record<string, unknown>) => {
|
|
148
|
+
const v = sc.favicon;
|
|
149
|
+
return isFaviconStyle(v) ? v : undefined;
|
|
150
|
+
},
|
|
151
|
+
toServer: (v: FaviconStyle) => ({ favicon: v }),
|
|
152
|
+
},
|
|
134
153
|
|
|
135
154
|
gridEnabled: {
|
|
136
155
|
// Default ON: plans open in the classic grid / floating-card look. The UI 2.0
|
|
@@ -205,6 +224,54 @@ export const SETTINGS = {
|
|
|
205
224
|
serverKey: undefined, fromServer: undefined, toServer: undefined,
|
|
206
225
|
},
|
|
207
226
|
|
|
227
|
+
// The view the user last SELECTED via the in-review header toggle. Layered
|
|
228
|
+
// between the session state and the persisted reviewPanelView default, so a
|
|
229
|
+
// new session opens on what the user was actually using. Cookie-only.
|
|
230
|
+
// null = no last-used recorded (fall through to reviewPanelView).
|
|
231
|
+
//
|
|
232
|
+
// 'commits' is never recorded here for the same reason reviewPanelView
|
|
233
|
+
// rejects it: the Commits view is session-only and never an opening view.
|
|
234
|
+
reviewPanelViewLastUsed: {
|
|
235
|
+
defaultValue: null as 'sections' | 'tree' | null,
|
|
236
|
+
fromCookie: () => {
|
|
237
|
+
const v = storage.getItem('plannotator-review-panel-view-last-used');
|
|
238
|
+
return v === 'tree' || v === 'sections' ? v : undefined;
|
|
239
|
+
},
|
|
240
|
+
toCookie: (v: 'sections' | 'tree' | null) => {
|
|
241
|
+
// The null default seeds through here on first load — "unrecorded" has
|
|
242
|
+
// no cookie representation, so write nothing.
|
|
243
|
+
if (v === 'sections' || v === 'tree') {
|
|
244
|
+
storage.setItem('plannotator-review-panel-view-last-used', v);
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
serverKey: undefined, fromServer: undefined, toServer: undefined,
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
// Compact left-panel preferences. These are deliberately cookie-only: they
|
|
251
|
+
// shape the local file-list chrome without changing review semantics or the
|
|
252
|
+
// repository state, and should follow the reviewer across review sessions.
|
|
253
|
+
reviewShowViewedControls: {
|
|
254
|
+
defaultValue: true as boolean,
|
|
255
|
+
fromCookie: () => {
|
|
256
|
+
const value = storage.getItem('plannotator-review-show-viewed-controls');
|
|
257
|
+
return value === 'true' ? true : value === 'false' ? false : undefined;
|
|
258
|
+
},
|
|
259
|
+
toCookie: (value: boolean) =>
|
|
260
|
+
storage.setItem('plannotator-review-show-viewed-controls', String(value)),
|
|
261
|
+
serverKey: undefined, fromServer: undefined, toServer: undefined,
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
reviewShowStageControls: {
|
|
265
|
+
defaultValue: true as boolean,
|
|
266
|
+
fromCookie: () => {
|
|
267
|
+
const value = storage.getItem('plannotator-review-show-stage-controls');
|
|
268
|
+
return value === 'true' ? true : value === 'false' ? false : undefined;
|
|
269
|
+
},
|
|
270
|
+
toCookie: (value: boolean) =>
|
|
271
|
+
storage.setItem('plannotator-review-show-stage-controls', String(value)),
|
|
272
|
+
serverKey: undefined, fromServer: undefined, toServer: undefined,
|
|
273
|
+
},
|
|
274
|
+
|
|
208
275
|
defaultDiffType: {
|
|
209
276
|
defaultValue: 'since-base' as 'since-base' | 'uncommitted' | 'unstaged' | 'staged' | 'merge-base' | 'all',
|
|
210
277
|
fromCookie: () => {
|
|
@@ -414,6 +481,36 @@ export const SETTINGS = {
|
|
|
414
481
|
fromServer: undefined,
|
|
415
482
|
toServer: undefined,
|
|
416
483
|
},
|
|
484
|
+
semanticDiffEnabled: {
|
|
485
|
+
defaultValue: true as boolean,
|
|
486
|
+
fromCookie: () => {
|
|
487
|
+
const value = storage.getItem('plannotator-semantic-diff-enabled');
|
|
488
|
+
return value === 'true' ? true : value === 'false' ? false : undefined;
|
|
489
|
+
},
|
|
490
|
+
toCookie: (value: boolean) =>
|
|
491
|
+
storage.setItem('plannotator-semantic-diff-enabled', String(value)),
|
|
492
|
+
serverKey: 'reviewAnalysis',
|
|
493
|
+
fromServer: (serverConfig: Record<string, unknown>) => {
|
|
494
|
+
const value = (serverConfig.reviewAnalysis as Record<string, unknown> | undefined)?.semanticDiff;
|
|
495
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
496
|
+
},
|
|
497
|
+
toServer: (value: boolean) => ({ reviewAnalysis: { semanticDiff: value } }),
|
|
498
|
+
},
|
|
499
|
+
callFlowEnabled: {
|
|
500
|
+
defaultValue: false as boolean,
|
|
501
|
+
fromCookie: () => {
|
|
502
|
+
const value = storage.getItem('plannotator-call-flow-enabled');
|
|
503
|
+
return value === 'true' ? true : value === 'false' ? false : undefined;
|
|
504
|
+
},
|
|
505
|
+
toCookie: (value: boolean) =>
|
|
506
|
+
storage.setItem('plannotator-call-flow-enabled', String(value)),
|
|
507
|
+
serverKey: 'reviewAnalysis',
|
|
508
|
+
fromServer: (serverConfig: Record<string, unknown>) => {
|
|
509
|
+
const value = (serverConfig.reviewAnalysis as Record<string, unknown> | undefined)?.callFlow;
|
|
510
|
+
return typeof value === 'boolean' ? value : undefined;
|
|
511
|
+
},
|
|
512
|
+
toServer: (value: boolean) => ({ reviewAnalysis: { callFlow: value } }),
|
|
513
|
+
},
|
|
417
514
|
conventionalComments: {
|
|
418
515
|
defaultValue: false as boolean,
|
|
419
516
|
fromCookie: () => {
|
|
@@ -453,6 +550,50 @@ export const SETTINGS = {
|
|
|
453
550
|
}
|
|
454
551
|
},
|
|
455
552
|
},
|
|
553
|
+
/**
|
|
554
|
+
* Where the annotate-mode Agent TUI docks: 'left' (where it always docked),
|
|
555
|
+
* 'right', or 'hidden' (no slot until the user opens it for the session).
|
|
556
|
+
*
|
|
557
|
+
* Server-synced so the placement survives the random port every annotate
|
|
558
|
+
* session runs on — a cookie alone is per-origin, and each invocation is a
|
|
559
|
+
* new origin, so a cookie-only preference is effectively per-session.
|
|
560
|
+
*
|
|
561
|
+
* The cookie key is the pre-registry one, so a user who already picked a
|
|
562
|
+
* side keeps it across the upgrade.
|
|
563
|
+
*/
|
|
564
|
+
agentTerminalSide: {
|
|
565
|
+
defaultValue: 'left' as AnnotateAgentTerminalSide,
|
|
566
|
+
fromCookie: () => {
|
|
567
|
+
const v = storage.getItem('plannotator-annotate-agent-terminal-side');
|
|
568
|
+
return isAnnotateAgentTerminalSide(v) ? v : undefined;
|
|
569
|
+
},
|
|
570
|
+
toCookie: (v: AnnotateAgentTerminalSide) =>
|
|
571
|
+
storage.setItem('plannotator-annotate-agent-terminal-side', v),
|
|
572
|
+
serverKey: 'agentTerminalSide',
|
|
573
|
+
fromServer: (sc: Record<string, unknown>) =>
|
|
574
|
+
isAnnotateAgentTerminalSide(sc.agentTerminalSide) ? sc.agentTerminalSide : undefined,
|
|
575
|
+
toServer: (v: AnnotateAgentTerminalSide) => ({ agentTerminalSide: v }),
|
|
576
|
+
},
|
|
577
|
+
/**
|
|
578
|
+
* Which agent the annotate-mode Agent TUI preselects. Empty string = no
|
|
579
|
+
* choice recorded yet, in which case the first available agent wins.
|
|
580
|
+
* Server-synced for the same reason as the placement above.
|
|
581
|
+
*/
|
|
582
|
+
agentTerminalDefaultAgent: {
|
|
583
|
+
defaultValue: '' as string,
|
|
584
|
+
fromCookie: () =>
|
|
585
|
+
storage.getItem('plannotator-annotate-agent-terminal-default') || undefined,
|
|
586
|
+
toCookie: (v: string) => {
|
|
587
|
+
if (v) storage.setItem('plannotator-annotate-agent-terminal-default', v);
|
|
588
|
+
else storage.removeItem('plannotator-annotate-agent-terminal-default');
|
|
589
|
+
},
|
|
590
|
+
serverKey: 'agentTerminalDefaultAgent',
|
|
591
|
+
fromServer: (sc: Record<string, unknown>) =>
|
|
592
|
+
typeof sc.agentTerminalDefaultAgent === 'string' && sc.agentTerminalDefaultAgent
|
|
593
|
+
? sc.agentTerminalDefaultAgent
|
|
594
|
+
: undefined,
|
|
595
|
+
toServer: (v: string) => ({ agentTerminalDefaultAgent: v }),
|
|
596
|
+
},
|
|
456
597
|
/* SettingDef<any>, not <unknown>: consumers compile this shipped source under
|
|
457
598
|
their own strictFunctionTypes, where a narrow `toCookie: (v: string) => void`
|
|
458
599
|
is contravariantly incompatible with `(value: unknown) => void`. */
|
|
@@ -59,14 +59,15 @@ export function useAIProviderConfig({
|
|
|
59
59
|
// Auto-resolve provider/model once capabilities are known.
|
|
60
60
|
useEffect(() => {
|
|
61
61
|
if (!available || providers.length === 0) return;
|
|
62
|
+
const saved = getAIProviderSettings();
|
|
63
|
+
const selection = resolveAIProviderSelection({
|
|
64
|
+
providers,
|
|
65
|
+
origin,
|
|
66
|
+
settings: saved,
|
|
67
|
+
serverDefaultProvider: defaultProvider,
|
|
68
|
+
});
|
|
69
|
+
|
|
62
70
|
setAiConfig(prev => {
|
|
63
|
-
const saved = getAIProviderSettings();
|
|
64
|
-
const selection = resolveAIProviderSelection({
|
|
65
|
-
providers,
|
|
66
|
-
origin,
|
|
67
|
-
settings: saved,
|
|
68
|
-
serverDefaultProvider: defaultProvider,
|
|
69
|
-
});
|
|
70
71
|
if (prev.providerId === selection.providerId && prev.model === selection.model) return prev;
|
|
71
72
|
return {
|
|
72
73
|
...prev,
|