@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
|
@@ -34,6 +34,17 @@ interface PlanHeaderMenuProps {
|
|
|
34
34
|
obsidianConfigured: boolean;
|
|
35
35
|
bearConfigured: boolean;
|
|
36
36
|
octarineConfigured: boolean;
|
|
37
|
+
compactTouchLayout?: boolean;
|
|
38
|
+
compactSessionActions?: CompactPlanAction[];
|
|
39
|
+
compactDocumentActions?: CompactPlanAction[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface CompactPlanAction {
|
|
43
|
+
id: 'exit' | 'feedback' | 'approve' | 'copy' | 'done' | 'edit' | 'tools' | 'annotate' | 'annotations' | 'ai' | 'review';
|
|
44
|
+
label: string;
|
|
45
|
+
subtitle?: string;
|
|
46
|
+
onSelect: () => void;
|
|
47
|
+
disabled?: boolean;
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
|
|
@@ -57,6 +68,9 @@ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
|
|
|
57
68
|
obsidianConfigured,
|
|
58
69
|
bearConfigured,
|
|
59
70
|
octarineConfigured,
|
|
71
|
+
compactTouchLayout = false,
|
|
72
|
+
compactSessionActions = [],
|
|
73
|
+
compactDocumentActions = [],
|
|
60
74
|
}) => {
|
|
61
75
|
const { theme, setTheme } = useTheme();
|
|
62
76
|
|
|
@@ -68,13 +82,20 @@ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
|
|
|
68
82
|
return (
|
|
69
83
|
<ActionMenu
|
|
70
84
|
panelWidth="wide"
|
|
85
|
+
panelClassName={compactTouchLayout
|
|
86
|
+
? 'absolute top-full right-0 mt-1 w-[min(18rem,calc(100vw-1rem))] max-h-[calc(var(--pn-viewport-height,100vh)-4.5rem-var(--pn-safe-top)-var(--pn-safe-bottom))] overflow-y-auto rounded-lg border border-border bg-popover py-1 shadow-xl z-[70]'
|
|
87
|
+
: undefined
|
|
88
|
+
}
|
|
71
89
|
renderTrigger={({ isOpen, toggleMenu }) => (
|
|
72
90
|
<button
|
|
91
|
+
id={compactTouchLayout ? 'pn-compact-plan-options-trigger' : undefined}
|
|
92
|
+
data-pn-touch-target={compactTouchLayout || undefined}
|
|
93
|
+
data-pn-touch-target-icon={compactTouchLayout || undefined}
|
|
73
94
|
onClick={() => {
|
|
74
95
|
if (!isOpen && showUpdateDot) updateInfo?.dismiss();
|
|
75
96
|
toggleMenu();
|
|
76
97
|
}}
|
|
77
|
-
className={`relative flex items-center gap-1.5 p-1.5 md:px-2.5 md:py-1
|
|
98
|
+
className={`relative flex items-center justify-center gap-1.5 rounded-md text-xs font-medium transition-colors ${compactTouchLayout ? 'h-11 w-11 p-0' : 'p-1.5 md:px-2.5 md:py-1'} ${
|
|
78
99
|
isOpen
|
|
79
100
|
? 'bg-muted text-foreground'
|
|
80
101
|
: 'text-muted-foreground hover:text-foreground hover:bg-muted'
|
|
@@ -99,11 +120,34 @@ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
|
|
|
99
120
|
>
|
|
100
121
|
{({ closeMenu }) => (
|
|
101
122
|
<>
|
|
123
|
+
{compactTouchLayout && compactSessionActions.length > 0 && (
|
|
124
|
+
<>
|
|
125
|
+
<CompactActionSection
|
|
126
|
+
label="Review"
|
|
127
|
+
actions={compactSessionActions}
|
|
128
|
+
closeMenu={closeMenu}
|
|
129
|
+
/>
|
|
130
|
+
<ActionMenuDivider />
|
|
131
|
+
</>
|
|
132
|
+
)}
|
|
133
|
+
|
|
134
|
+
{compactTouchLayout && compactDocumentActions.length > 0 && (
|
|
135
|
+
<>
|
|
136
|
+
<CompactActionSection
|
|
137
|
+
label="Document"
|
|
138
|
+
actions={compactDocumentActions}
|
|
139
|
+
closeMenu={closeMenu}
|
|
140
|
+
/>
|
|
141
|
+
<ActionMenuDivider />
|
|
142
|
+
</>
|
|
143
|
+
)}
|
|
144
|
+
|
|
102
145
|
<div className="px-3 py-2 space-y-1.5">
|
|
103
146
|
<ActionMenuSectionLabel>Theme</ActionMenuSectionLabel>
|
|
104
147
|
<div className="flex items-center gap-1 rounded-lg bg-muted/50 p-0.5">
|
|
105
148
|
{THEME_MODES.map(({ id, label, Icon }) => (
|
|
106
149
|
<button
|
|
150
|
+
data-pn-touch-target={compactTouchLayout || undefined}
|
|
107
151
|
key={id}
|
|
108
152
|
onClick={() => {
|
|
109
153
|
closeMenu();
|
|
@@ -243,6 +287,101 @@ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
|
|
|
243
287
|
);
|
|
244
288
|
};
|
|
245
289
|
|
|
290
|
+
const CompactActionSection = ({
|
|
291
|
+
label,
|
|
292
|
+
actions,
|
|
293
|
+
closeMenu,
|
|
294
|
+
}: {
|
|
295
|
+
label: string;
|
|
296
|
+
actions: CompactPlanAction[];
|
|
297
|
+
closeMenu: () => void;
|
|
298
|
+
}) => (
|
|
299
|
+
<div className="py-1">
|
|
300
|
+
<div className="px-3 py-1">
|
|
301
|
+
<ActionMenuSectionLabel>{label}</ActionMenuSectionLabel>
|
|
302
|
+
</div>
|
|
303
|
+
{actions.map((action) => (
|
|
304
|
+
<ActionMenuItem
|
|
305
|
+
key={action.id}
|
|
306
|
+
onClick={() => {
|
|
307
|
+
closeMenu();
|
|
308
|
+
action.onSelect();
|
|
309
|
+
}}
|
|
310
|
+
disabled={action.disabled}
|
|
311
|
+
icon={<CompactPlanActionIcon kind={action.id} />}
|
|
312
|
+
label={action.label}
|
|
313
|
+
subtitle={action.subtitle}
|
|
314
|
+
/>
|
|
315
|
+
))}
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const CompactPlanActionIcon = ({ kind }: { kind: CompactPlanAction['id'] }) => {
|
|
320
|
+
if (kind === 'review') {
|
|
321
|
+
return (
|
|
322
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
323
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 11l2 2 4-4m6 3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
324
|
+
</svg>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
if (kind === 'annotations') {
|
|
328
|
+
return (
|
|
329
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
330
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
|
331
|
+
</svg>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (kind === 'ai') {
|
|
335
|
+
return (
|
|
336
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.8}>
|
|
337
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 3l1.2 3.8L17 8l-3.8 1.2L12 13l-1.2-3.8L7 8l3.8-1.2L12 3zM18.5 13l.8 2.7L22 16.5l-2.7.8-.8 2.7-.8-2.7-2.7-.8 2.7-.8.8-2.7zM5 13l.7 2.3L8 16l-2.3.7L5 19l-.7-2.3L2 16l2.3-.7L5 13z" />
|
|
338
|
+
</svg>
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
if (kind === 'approve' || kind === 'done') {
|
|
342
|
+
return (
|
|
343
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
344
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
|
|
345
|
+
</svg>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
if (kind === 'feedback') {
|
|
349
|
+
return (
|
|
350
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
351
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
|
352
|
+
</svg>
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
if (kind === 'copy') {
|
|
356
|
+
return (
|
|
357
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
358
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
|
359
|
+
</svg>
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (kind === 'edit' || kind === 'annotate') {
|
|
363
|
+
return (
|
|
364
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
365
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
366
|
+
</svg>
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (kind === 'tools') {
|
|
370
|
+
// Eye, matching the desktop header's show/hide-tools toggle.
|
|
371
|
+
return (
|
|
372
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
373
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M2.036 12.322a1.012 1.012 0 010-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178z" />
|
|
374
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
|
375
|
+
</svg>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
return (
|
|
379
|
+
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
380
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
381
|
+
</svg>
|
|
382
|
+
);
|
|
383
|
+
};
|
|
384
|
+
|
|
246
385
|
const MenuIcon = () => (
|
|
247
386
|
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
248
387
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
|
|
@@ -107,6 +107,7 @@ export function SearchableSelect<T extends { id: string }>({
|
|
|
107
107
|
/>
|
|
108
108
|
</svg>
|
|
109
109
|
<input
|
|
110
|
+
data-pn-mobile-editable
|
|
110
111
|
ref={inputRef}
|
|
111
112
|
value={search}
|
|
112
113
|
onChange={(e) => {
|
|
@@ -128,6 +129,7 @@ export function SearchableSelect<T extends { id: string }>({
|
|
|
128
129
|
) : (
|
|
129
130
|
filtered.map((item, i) => (
|
|
130
131
|
<button
|
|
132
|
+
data-pn-touch-target
|
|
131
133
|
key={item.id}
|
|
132
134
|
type="button"
|
|
133
135
|
data-index={i}
|
package/components/Settings.tsx
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
|
+
import type { AnnotateAgentTerminalSide } from '@plannotator/core/agent-terminal';
|
|
3
4
|
import type { Origin } from '@plannotator/core/agents';
|
|
4
5
|
import type { DiffLineBgIntensity } from '@plannotator/core/config-types';
|
|
5
6
|
import { configStore, useConfigValue, setReviewPanelView, setReviewDefaultDiffType } from '../config';
|
|
7
|
+
import { setWebMcpToolsEnabled, useWebMcpToolsEnabled } from '../webmcp/preference';
|
|
6
8
|
import { loadDiffFont } from '../utils/diffFonts';
|
|
7
9
|
import { TaterSpritePullup } from './TaterSpritePullup';
|
|
8
10
|
import { getIdentity, regenerateIdentity, setCustomIdentity, isIdentityEditable } from '../utils/identity';
|
|
@@ -73,8 +75,9 @@ import {
|
|
|
73
75
|
type FileBrowserSettings,
|
|
74
76
|
} from '../utils/fileBrowser';
|
|
75
77
|
import { requestVimDocumentFocus } from '../hooks/useVimDocumentFocus';
|
|
78
|
+
import { AnalysisLayerToggle } from './AnalysisLayerToggle';
|
|
76
79
|
|
|
77
|
-
type SettingsTab = 'general' | 'theme' | 'git' | 'display' | 'saving' | 'labels' | 'vim' | 'shortcuts' | 'ai' | 'files' | 'obsidian' | 'bear' | 'octarine' | 'comments' | 'hooks';
|
|
80
|
+
type SettingsTab = 'general' | 'theme' | 'git' | 'display' | 'analysis' | 'saving' | 'labels' | 'vim' | 'shortcuts' | 'ai' | 'files' | 'obsidian' | 'bear' | 'octarine' | 'comments' | 'hooks';
|
|
78
81
|
|
|
79
82
|
interface SettingsProps {
|
|
80
83
|
taterMode: boolean;
|
|
@@ -94,8 +97,21 @@ interface SettingsProps {
|
|
|
94
97
|
* (base ref unresolvable) — the Git tab shows a note that the Git-status
|
|
95
98
|
* preference can't take effect in THIS repo. */
|
|
96
99
|
sinceBaseUnavailable?: boolean;
|
|
100
|
+
/** The host is rendering its compact touch shell (review only). Display
|
|
101
|
+
* settings that the compact shell overrides for the session are hidden
|
|
102
|
+
* there instead of silently editing the desktop preference. */
|
|
103
|
+
isCompactTouchLayout?: boolean;
|
|
97
104
|
/** Override Obsidian vault detection (default = GET /api/obsidian/vaults). */
|
|
98
105
|
onDetectObsidianVaults?: () => Promise<string[]>;
|
|
106
|
+
/** This annotate session actually offers the Agent TUI, so its Position
|
|
107
|
+
* setting is worth showing. Default false: the setting is the only way back
|
|
108
|
+
* from a Hidden position, but offering it where no terminal can ever run
|
|
109
|
+
* would just be a dead control. */
|
|
110
|
+
agentTerminalAvailable?: boolean;
|
|
111
|
+
/** The browser exposes WebMCP (`document.modelContext`), so the "Agent
|
|
112
|
+
* tools" opt-out is worth showing. Default false: a browser without the
|
|
113
|
+
* API gets no row at all (the provider registers nothing there). */
|
|
114
|
+
webmcpAvailable?: boolean;
|
|
99
115
|
}
|
|
100
116
|
|
|
101
117
|
// --- Review-mode Display tab (diff display options) ---
|
|
@@ -148,6 +164,12 @@ const DEFAULT_DIFF_TYPE_OPTIONS = [
|
|
|
148
164
|
{ value: 'all' as const, label: 'All Files (HEAD)', description: "Every tracked file at HEAD, shown as additions" },
|
|
149
165
|
];
|
|
150
166
|
|
|
167
|
+
const AGENT_TERMINAL_SIDE_OPTIONS: { value: AnnotateAgentTerminalSide; label: string }[] = [
|
|
168
|
+
{ value: 'left', label: 'Left' },
|
|
169
|
+
{ value: 'right', label: 'Right' },
|
|
170
|
+
{ value: 'hidden', label: 'Hidden' },
|
|
171
|
+
];
|
|
172
|
+
|
|
151
173
|
function SegmentedControl<T extends string>({ options, value, onChange }: {
|
|
152
174
|
options: { value: T; label: string }[];
|
|
153
175
|
value: T;
|
|
@@ -192,12 +214,17 @@ function ToggleSwitch({ checked, onChange, label, description, disabled = false
|
|
|
192
214
|
aria-checked={checked}
|
|
193
215
|
disabled={disabled}
|
|
194
216
|
onClick={() => onChange(!checked)}
|
|
195
|
-
className=
|
|
196
|
-
checked ? 'bg-primary' : 'bg-muted'
|
|
197
|
-
}`}
|
|
217
|
+
className="relative inline-flex h-11 w-11 shrink-0 items-center disabled:cursor-not-allowed"
|
|
198
218
|
>
|
|
199
219
|
<span
|
|
200
|
-
|
|
220
|
+
aria-hidden="true"
|
|
221
|
+
className={`absolute inset-x-0 top-2.5 h-6 rounded-full transition-colors ${
|
|
222
|
+
checked ? 'bg-primary' : 'bg-muted'
|
|
223
|
+
}`}
|
|
224
|
+
/>
|
|
225
|
+
<span
|
|
226
|
+
aria-hidden="true"
|
|
227
|
+
className={`absolute top-3.5 inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
|
|
201
228
|
checked ? 'translate-x-6' : 'translate-x-1'
|
|
202
229
|
}`}
|
|
203
230
|
/>
|
|
@@ -288,6 +315,63 @@ function VimSettingsTab({
|
|
|
288
315
|
);
|
|
289
316
|
}
|
|
290
317
|
|
|
318
|
+
function ReviewAnalysisTab() {
|
|
319
|
+
const semanticDiffEnabled = useConfigValue('semanticDiffEnabled');
|
|
320
|
+
const callFlowEnabled = useConfigValue('callFlowEnabled');
|
|
321
|
+
return (
|
|
322
|
+
<div className="space-y-5">
|
|
323
|
+
<div>
|
|
324
|
+
<div className="text-sm font-semibold">Analysis layers</div>
|
|
325
|
+
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
|
326
|
+
Add structural context to the ordinary code diff. Each layer is independent and does no work while disabled.
|
|
327
|
+
</p>
|
|
328
|
+
</div>
|
|
329
|
+
|
|
330
|
+
<div className="space-y-4 rounded-xl border border-border bg-muted/20 p-4">
|
|
331
|
+
<AnalysisLayerToggle
|
|
332
|
+
checked={semanticDiffEnabled}
|
|
333
|
+
onChange={(enabled) => configStore.set('semanticDiffEnabled', enabled)}
|
|
334
|
+
label="Semantic changes"
|
|
335
|
+
description="Organize added, changed, moved, and removed functions, classes, and other named code. Enabled by default."
|
|
336
|
+
/>
|
|
337
|
+
<div className="border-t border-border" />
|
|
338
|
+
<AnalysisLayerToggle
|
|
339
|
+
checked={callFlowEnabled}
|
|
340
|
+
onChange={(enabled) => configStore.set('callFlowEnabled', enabled)}
|
|
341
|
+
label="Call flow"
|
|
342
|
+
description="Diffs for function call stacks across git commits. 22 languages supported (AST-based, built using Tree-sitter)."
|
|
343
|
+
/>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<div className="grid gap-3 sm:grid-cols-2">
|
|
347
|
+
<div className="rounded-xl border border-border p-3.5">
|
|
348
|
+
<div className="flex items-center gap-2 text-xs font-semibold">
|
|
349
|
+
<span className="grid h-6 w-6 place-items-center rounded-md bg-primary/10 font-mono text-primary">▣</span>
|
|
350
|
+
Dock
|
|
351
|
+
</div>
|
|
352
|
+
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
|
|
353
|
+
Open the complete analysis from the file panel. Rows jump to the relevant code location.
|
|
354
|
+
</p>
|
|
355
|
+
</div>
|
|
356
|
+
<div className="rounded-xl border border-border p-3.5">
|
|
357
|
+
<div className="flex items-center gap-2 text-xs font-semibold">
|
|
358
|
+
<span className="grid h-6 w-6 place-items-center rounded-md bg-primary/10 font-mono text-primary">⌁</span>
|
|
359
|
+
File lens
|
|
360
|
+
</div>
|
|
361
|
+
<p className="mt-2 text-xs leading-relaxed text-muted-foreground">
|
|
362
|
+
File headers show compact counts and a focused popover for that file.
|
|
363
|
+
</p>
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
|
|
367
|
+
<div className="rounded-xl border border-warning/25 bg-warning/[0.05] p-4 text-xs leading-relaxed text-muted-foreground">
|
|
368
|
+
<span className="font-semibold text-foreground">Call flow is syntactic.</span>{' '}
|
|
369
|
+
It does not use type resolution or runtime traces, so treat it as navigation context—not proof that a path executes.
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
291
375
|
const GitTab: React.FC<{ sinceBaseUnavailable?: boolean }> = ({ sinceBaseUnavailable }) => {
|
|
292
376
|
const defaultDiffType = useConfigValue('defaultDiffType');
|
|
293
377
|
const reviewPanelView = useConfigValue('reviewPanelView');
|
|
@@ -358,7 +442,7 @@ const GitTab: React.FC<{ sinceBaseUnavailable?: boolean }> = ({ sinceBaseUnavail
|
|
|
358
442
|
);
|
|
359
443
|
};
|
|
360
444
|
|
|
361
|
-
const ReviewDisplayTab: React.FC = () => {
|
|
445
|
+
const ReviewDisplayTab: React.FC<{ isCompactTouchLayout?: boolean }> = ({ isCompactTouchLayout = false }) => {
|
|
362
446
|
const diffStyle = useConfigValue('diffStyle');
|
|
363
447
|
const diffOverflow = useConfigValue('diffOverflow');
|
|
364
448
|
const diffIndicators = useConfigValue('diffIndicators');
|
|
@@ -456,13 +540,23 @@ const ReviewDisplayTab: React.FC = () => {
|
|
|
456
540
|
|
|
457
541
|
<div className="border-t border-border" />
|
|
458
542
|
|
|
459
|
-
{/* Diff Style
|
|
543
|
+
{/* Diff Style. The compact touch shell renders a session-only unified
|
|
544
|
+
diff, so the control there would look dead while quietly rewriting
|
|
545
|
+
the DESKTOP preference. Say what the phone is doing instead. */}
|
|
460
546
|
<div className="space-y-2">
|
|
461
547
|
<div>
|
|
462
548
|
<div className="text-sm font-medium">Diff Style</div>
|
|
463
549
|
<div className="text-xs text-muted-foreground">Side-by-side or inline diff view</div>
|
|
550
|
+
{isCompactTouchLayout && (
|
|
551
|
+
<div className="text-xs text-muted-foreground mt-1">
|
|
552
|
+
This layout shows unified diffs for the session; your desktop
|
|
553
|
+
preference is unchanged.
|
|
554
|
+
</div>
|
|
555
|
+
)}
|
|
464
556
|
</div>
|
|
465
|
-
|
|
557
|
+
{!isCompactTouchLayout && (
|
|
558
|
+
<SegmentedControl options={DIFF_STYLE_OPTIONS} value={diffStyle} onChange={(v) => configStore.set('diffStyle', v)} />
|
|
559
|
+
)}
|
|
466
560
|
</div>
|
|
467
561
|
|
|
468
562
|
<div className="border-t border-border" />
|
|
@@ -759,7 +853,8 @@ const CommentsTab: React.FC = () => {
|
|
|
759
853
|
);
|
|
760
854
|
};
|
|
761
855
|
|
|
762
|
-
export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange, onIdentityChange, origin, mode = 'plan', onUIPreferencesChange, externalOpen, onExternalClose, aiProviders = [], gitUser, sinceBaseUnavailable, onDetectObsidianVaults }) => {
|
|
856
|
+
export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange, onIdentityChange, origin, mode = 'plan', onUIPreferencesChange, externalOpen, onExternalClose, aiProviders = [], gitUser, sinceBaseUnavailable, isCompactTouchLayout = false, onDetectObsidianVaults, agentTerminalAvailable = false, webmcpAvailable = false }) => {
|
|
857
|
+
const webmcpTools = useWebMcpToolsEnabled();
|
|
763
858
|
const [showDialog, setShowDialog] = useState(false);
|
|
764
859
|
const settingsWasOpenRef = useRef(false);
|
|
765
860
|
const [themePreview, setThemePreview] = useState(false);
|
|
@@ -791,6 +886,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
791
886
|
const vimModeEnabled = useConfigValue('vimModeEnabled');
|
|
792
887
|
const vimHudEnabled = useConfigValue('vimHudEnabled');
|
|
793
888
|
const vimHudKeyPanelEnabled = useConfigValue('vimHudKeyPanelEnabled');
|
|
889
|
+
const agentTerminalSide = useConfigValue('agentTerminalSide');
|
|
794
890
|
const [identity, setIdentity] = useState('');
|
|
795
891
|
const [obsidian, setObsidian] = useState<ObsidianSettings>({
|
|
796
892
|
enabled: false,
|
|
@@ -835,6 +931,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
835
931
|
if (mode === 'review') {
|
|
836
932
|
t.push({ id: 'git', label: 'Git' });
|
|
837
933
|
t.push({ id: 'display', label: 'Editor' });
|
|
934
|
+
t.push({ id: 'analysis', label: 'Analysis' });
|
|
838
935
|
t.push({ id: 'comments', label: 'Comments' });
|
|
839
936
|
if (aiProviders.length > 0) {
|
|
840
937
|
t.push({ id: 'ai', label: 'AI' });
|
|
@@ -1191,6 +1288,39 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1191
1288
|
</div>
|
|
1192
1289
|
</div>
|
|
1193
1290
|
|
|
1291
|
+
{/* Agent tools (WebMCP). Shown only when the browser
|
|
1292
|
+
exposes document.modelContext; off unregisters the
|
|
1293
|
+
tools for this browser. Cookie-only and idle at its
|
|
1294
|
+
default: no cookie exists until the user opts out. */}
|
|
1295
|
+
{webmcpAvailable && (
|
|
1296
|
+
<>
|
|
1297
|
+
<div className="border-t border-border" />
|
|
1298
|
+
<div className="flex items-center justify-between" data-webmcp-setting="true">
|
|
1299
|
+
<div>
|
|
1300
|
+
<div className="text-sm font-medium">Agent tools</div>
|
|
1301
|
+
<div className="text-xs text-muted-foreground">
|
|
1302
|
+
Let your browser's agent read this document and leave comments. Approving and sending feedback stay yours.
|
|
1303
|
+
</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
<button
|
|
1306
|
+
role="switch"
|
|
1307
|
+
aria-checked={webmcpTools}
|
|
1308
|
+
aria-label="Agent tools"
|
|
1309
|
+
onClick={() => setWebMcpToolsEnabled(!webmcpTools)}
|
|
1310
|
+
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
1311
|
+
webmcpTools ? 'bg-primary' : 'bg-muted'
|
|
1312
|
+
}`}
|
|
1313
|
+
>
|
|
1314
|
+
<span
|
|
1315
|
+
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
|
|
1316
|
+
webmcpTools ? 'translate-x-6' : 'translate-x-1'
|
|
1317
|
+
}`}
|
|
1318
|
+
/>
|
|
1319
|
+
</button>
|
|
1320
|
+
</div>
|
|
1321
|
+
</>
|
|
1322
|
+
)}
|
|
1323
|
+
|
|
1194
1324
|
{/* Permission Mode (Claude Code only) */}
|
|
1195
1325
|
{origin === 'claude-code' && mode === 'plan' && (
|
|
1196
1326
|
<>
|
|
@@ -1306,6 +1436,37 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1306
1436
|
</>
|
|
1307
1437
|
)}
|
|
1308
1438
|
|
|
1439
|
+
{/* Agent TUI position. The same control lives in the
|
|
1440
|
+
terminal's own Display popover, but that one is
|
|
1441
|
+
unreachable once the position is Hidden — this is the
|
|
1442
|
+
way back. Both write the same config value. Gated on the
|
|
1443
|
+
terminal actually being available in this session, so a
|
|
1444
|
+
remote or runtime-less annotate never offers a dead
|
|
1445
|
+
control. */}
|
|
1446
|
+
{mode === 'annotate' && agentTerminalAvailable && (
|
|
1447
|
+
<>
|
|
1448
|
+
<div className="border-t border-border" />
|
|
1449
|
+
<div className="space-y-2">
|
|
1450
|
+
<div>
|
|
1451
|
+
<div className="text-sm font-medium">Agent TUI Position</div>
|
|
1452
|
+
<div className="text-xs text-muted-foreground">
|
|
1453
|
+
Which side the agent terminal docks on, or Hidden to keep it out of the layout
|
|
1454
|
+
</div>
|
|
1455
|
+
</div>
|
|
1456
|
+
<SegmentedControl
|
|
1457
|
+
options={AGENT_TERMINAL_SIDE_OPTIONS}
|
|
1458
|
+
value={agentTerminalSide}
|
|
1459
|
+
onChange={(v) => configStore.set('agentTerminalSide', v)}
|
|
1460
|
+
/>
|
|
1461
|
+
{agentTerminalSide === 'hidden' && (
|
|
1462
|
+
<div className="text-[10px] text-muted-foreground/70">
|
|
1463
|
+
You can still open it for this session from the sidebar rail.
|
|
1464
|
+
</div>
|
|
1465
|
+
)}
|
|
1466
|
+
</div>
|
|
1467
|
+
</>
|
|
1468
|
+
)}
|
|
1469
|
+
|
|
1309
1470
|
<div className="border-t border-border" />
|
|
1310
1471
|
|
|
1311
1472
|
{/* Auto-close Tab */}
|
|
@@ -1343,7 +1504,11 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1343
1504
|
|
|
1344
1505
|
{/* === DISPLAY TAB === */}
|
|
1345
1506
|
{activeTab === 'display' && mode === 'review' && (
|
|
1346
|
-
<ReviewDisplayTab />
|
|
1507
|
+
<ReviewDisplayTab isCompactTouchLayout={isCompactTouchLayout} />
|
|
1508
|
+
)}
|
|
1509
|
+
|
|
1510
|
+
{activeTab === 'analysis' && mode === 'review' && (
|
|
1511
|
+
<ReviewAnalysisTab />
|
|
1347
1512
|
)}
|
|
1348
1513
|
|
|
1349
1514
|
{activeTab === 'display' && mode !== 'review' && (
|
|
@@ -9,6 +9,8 @@ const ROOT_LABELS: Record<SkillCatalogEntry['root'], string> = {
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
interface SkillReferenceMenuProps {
|
|
12
|
+
/** Id referenced by the focused textarea's aria-controls attribute. */
|
|
13
|
+
id: string;
|
|
12
14
|
items: SkillCatalogEntry[];
|
|
13
15
|
/** Explicitly activated row, or null — the menu opens with nothing active. */
|
|
14
16
|
activeIndex: number | null;
|
|
@@ -79,6 +81,7 @@ function computeMenuPlacement(
|
|
|
79
81
|
* a purely visual affordance; a pointer click inserts directly.
|
|
80
82
|
*/
|
|
81
83
|
export const SkillReferenceMenu: React.FC<SkillReferenceMenuProps> = ({
|
|
84
|
+
id,
|
|
82
85
|
items,
|
|
83
86
|
activeIndex,
|
|
84
87
|
onSelect,
|
|
@@ -140,6 +143,8 @@ export const SkillReferenceMenu: React.FC<SkillReferenceMenuProps> = ({
|
|
|
140
143
|
return (
|
|
141
144
|
<div
|
|
142
145
|
ref={menuRef}
|
|
146
|
+
id={id}
|
|
147
|
+
role="listbox"
|
|
143
148
|
data-skill-menu="true"
|
|
144
149
|
data-skill-menu-placement={placement.direction}
|
|
145
150
|
data-popover-layer="true"
|
|
@@ -157,6 +162,10 @@ export const SkillReferenceMenu: React.FC<SkillReferenceMenuProps> = ({
|
|
|
157
162
|
<button
|
|
158
163
|
key={item.name}
|
|
159
164
|
type="button"
|
|
165
|
+
id={`${id}-option-${index}`}
|
|
166
|
+
role="option"
|
|
167
|
+
aria-selected={index === activeIndex}
|
|
168
|
+
tabIndex={-1}
|
|
160
169
|
data-skill-item={item.name}
|
|
161
170
|
data-skill-item-active={index === activeIndex ? 'true' : undefined}
|
|
162
171
|
// Insert on pointerdown so the textarea never loses focus. A
|
|
@@ -27,7 +27,10 @@
|
|
|
27
27
|
import React, { useEffect, useRef, useState } from 'react';
|
|
28
28
|
import { AnnotationToolstrip } from './AnnotationToolstrip';
|
|
29
29
|
import { DocBadges } from './DocBadges';
|
|
30
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
getScrollViewportIntersectionRoot,
|
|
32
|
+
useScrollViewport,
|
|
33
|
+
} from '../hooks/useScrollViewport';
|
|
31
34
|
import type { EditorMode, InputMethod } from '../types';
|
|
32
35
|
import type { PlanDiffStats } from '../utils/planDiffEngine';
|
|
33
36
|
|
|
@@ -172,7 +175,11 @@ export const StickyHeaderLane: React.FC<StickyHeaderLaneProps> = ({
|
|
|
172
175
|
if (!sentinelRef.current || !scrollViewport) return;
|
|
173
176
|
const observer = new IntersectionObserver(
|
|
174
177
|
([entry]) => setIsStuck(!entry.isIntersecting),
|
|
175
|
-
{
|
|
178
|
+
{
|
|
179
|
+
root: getScrollViewportIntersectionRoot(scrollViewport),
|
|
180
|
+
rootMargin: '80px 0px 0px 0px',
|
|
181
|
+
threshold: 0,
|
|
182
|
+
}
|
|
176
183
|
);
|
|
177
184
|
observer.observe(sentinelRef.current);
|
|
178
185
|
return () => observer.disconnect();
|
|
@@ -5,7 +5,12 @@ import {
|
|
|
5
5
|
getAnnotationCountBySection,
|
|
6
6
|
type TocItem,
|
|
7
7
|
} from '../utils/annotationHelpers';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getScrollViewportRect,
|
|
10
|
+
getScrollViewportTop,
|
|
11
|
+
scrollViewportTo,
|
|
12
|
+
useScrollViewport,
|
|
13
|
+
} from '../hooks/useScrollViewport';
|
|
9
14
|
|
|
10
15
|
interface TableOfContentsProps {
|
|
11
16
|
blocks: Block[];
|
|
@@ -82,11 +87,11 @@ export function TableOfContents({
|
|
|
82
87
|
if (target && scrollViewport) {
|
|
83
88
|
const scrollContainer = scrollViewport;
|
|
84
89
|
const headerOffset = 80; // sticky header (h-12) + breathing room
|
|
85
|
-
const containerRect = scrollContainer
|
|
90
|
+
const containerRect = getScrollViewportRect(scrollContainer);
|
|
86
91
|
const targetRect = target.getBoundingClientRect();
|
|
87
92
|
const offsetPosition =
|
|
88
|
-
scrollContainer
|
|
89
|
-
scrollContainer
|
|
93
|
+
getScrollViewportTop(scrollContainer) + (targetRect.top - containerRect.top) - headerOffset;
|
|
94
|
+
scrollViewportTo(scrollContainer, { top: offsetPosition, behavior: 'smooth' });
|
|
90
95
|
}
|
|
91
96
|
},
|
|
92
97
|
[onNavigate, scrollViewport]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useMemo, type JSX } from 'react';
|
|
2
|
-
import { motion } from 'motion/react';
|
|
2
|
+
import { motion, useReducedMotion } from 'motion/react';
|
|
3
3
|
|
|
4
4
|
export interface TextShimmerProps {
|
|
5
5
|
children: string;
|
|
@@ -16,6 +16,7 @@ function TextShimmerComponent({
|
|
|
16
16
|
duration = 2,
|
|
17
17
|
spread = 2,
|
|
18
18
|
}: TextShimmerProps) {
|
|
19
|
+
const shouldReduceMotion = useReducedMotion();
|
|
19
20
|
const MotionComponent = motion.create(
|
|
20
21
|
Component as keyof JSX.IntrinsicElements
|
|
21
22
|
);
|
|
@@ -35,9 +36,9 @@ function TextShimmerComponent({
|
|
|
35
36
|
]
|
|
36
37
|
.filter(Boolean)
|
|
37
38
|
.join(' ')}
|
|
38
|
-
initial={{ backgroundPosition: '100% center' }}
|
|
39
|
-
animate={{ backgroundPosition: '0% center' }}
|
|
40
|
-
transition={{
|
|
39
|
+
initial={shouldReduceMotion ? false : { backgroundPosition: '100% center' }}
|
|
40
|
+
animate={shouldReduceMotion ? undefined : { backgroundPosition: '0% center' }}
|
|
41
|
+
transition={shouldReduceMotion ? undefined : {
|
|
41
42
|
repeat: Infinity,
|
|
42
43
|
duration,
|
|
43
44
|
ease: 'linear',
|
|
@@ -45,7 +46,9 @@ function TextShimmerComponent({
|
|
|
45
46
|
style={
|
|
46
47
|
{
|
|
47
48
|
'--spread': `${dynamicSpread}px`,
|
|
48
|
-
backgroundImage:
|
|
49
|
+
backgroundImage: shouldReduceMotion
|
|
50
|
+
? 'linear-gradient(var(--base-color), var(--base-color))'
|
|
51
|
+
: 'var(--bg), linear-gradient(var(--base-color), var(--base-color))',
|
|
49
52
|
} as React.CSSProperties
|
|
50
53
|
}
|
|
51
54
|
>
|