@plannotator/ui 0.53.0 → 0.54.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/HANDOFF.md +284 -27
- package/README.md +84 -14
- package/components/AgentToolAnnouncementDialog.tsx +1 -1
- package/components/AgentsTab.tsx +1 -1
- package/components/AnnotationPanel.tsx +177 -34
- package/components/AnnotationSidebar.tsx +1 -1
- package/components/AnnotationToolbar.tsx +64 -9
- package/components/AnnotationToolstrip.tsx +45 -40
- package/components/ApproveDropdown.tsx +7 -9
- package/components/AskSessionAnnouncementDialog.tsx +1 -1
- package/components/AttachmentsButton.tsx +1 -1
- package/components/BlockRenderer.tsx +19 -5
- package/components/CodeFilePopout.tsx +140 -5
- package/components/CommentPopover.tsx +46 -8
- package/components/DecisionControl.tsx +31 -5
- package/components/DiagramBlock.tsx +17 -12
- package/components/EditorAnnotationCard.tsx +1 -1
- package/components/ExportModal.tsx +77 -7
- package/components/HostTitle.tsx +0 -4
- package/components/HtmlSurfaceControls.tsx +2 -1
- package/components/ImageAnnotator/Toolbar.tsx +25 -1
- package/components/ImageAnnotator/index.tsx +24 -4
- package/components/ImageLightbox.tsx +658 -120
- package/components/ImagePeek.tsx +38 -4
- package/components/ImageRegions.tsx +369 -0
- package/components/ImageThumbnail.tsx +1 -1
- package/components/ImageZoomStage.tsx +796 -0
- package/components/InboxAnnouncementDialog.tsx +202 -0
- package/components/InlineMarkdown.tsx +152 -24
- package/components/KeyboardShortcuts.tsx +23 -0
- package/components/LazyBlockBoundary.tsx +100 -0
- package/components/ListMarker.tsx +3 -3
- package/components/LookAndFeelAnnouncementDialog.tsx +12 -2
- package/components/MentionPicker.tsx +51 -0
- package/components/MenuVersionSection.tsx +31 -9
- package/components/PlanDecisionMenuExtras.tsx +129 -0
- package/components/PlanHeaderMenu.tsx +15 -1
- package/components/ProviderIcons.tsx +8 -0
- package/components/RefHoverCard.tsx +913 -0
- package/components/SendFailureToast.tsx +126 -0
- package/components/Settings.tsx +617 -126
- package/components/SkillReferenceMenu.tsx +39 -12
- package/components/TerminalToolsAnnouncementDialog.tsx +1 -1
- package/components/ThemeProvider.tsx +14 -1
- package/components/ThemeTab.tsx +16 -24
- package/components/Tooltip.tsx +49 -18
- package/components/Viewer.tsx +316 -47
- package/components/ai/DocumentAIChatPanel.tsx +29 -3
- package/components/ai/NewChatButton.tsx +74 -0
- package/components/blocks/CodeBlock.tsx +1 -1
- package/components/blocks/HtmlBlock.tsx +7 -1
- package/components/blocks/QuestionBlock.tsx +522 -62
- package/components/blocks/QuestionContext.tsx +3 -0
- package/components/diagram/DiagramComposer.tsx +5 -1
- package/components/diagram/DiagramPending.tsx +12 -2
- package/components/diagram/DiagramViewer.tsx +19 -3
- package/components/html-embed/HtmlEmbedBlock.tsx +374 -0
- package/components/html-embed/SvgFenceBlock.tsx +46 -0
- package/components/html-embed/index.ts +3 -0
- package/components/html-embed/languages.ts +15 -0
- package/components/html-embed/loadGate.ts +107 -0
- package/components/html-viewer/HtmlViewer.tsx +69 -6
- package/components/html-viewer/bridge-script.asset.js +504 -12
- package/components/html-viewer/bridge-script.lite.ts +1 -1
- package/components/html-viewer/bridge-script.ts +545 -12
- package/components/html-viewer/useHtmlAnnotation.ts +125 -0
- package/components/imageZoom.ts +265 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +1 -1
- package/components/plan-diff/PlanDiffViewer.tsx +13 -2
- package/components/plan-diff/PlanRawDiffView.tsx +1 -1
- package/components/sidebar/FileBrowser.tsx +16 -2
- package/components/sidebar/SidebarTabs.tsx +3 -0
- package/config/index.ts +3 -0
- package/config/reviewView.ts +41 -1
- package/config/settings.ts +155 -4
- package/configure.ts +28 -0
- package/hooks/pfm/useCodeFilePopout.ts +7 -1
- package/hooks/useAIChat.ts +72 -25
- package/hooks/useAnnotationDraft.ts +71 -6
- package/hooks/useAnnotationHighlighter.ts +57 -3
- package/hooks/useCodeAnnotationDraft.ts +113 -17
- package/hooks/useCookieHostSetting.ts +96 -0
- package/hooks/useDocumentFont.ts +17 -0
- package/hooks/useDocumentTextSize.ts +20 -0
- package/hooks/useFileBrowser.ts +10 -1
- package/hooks/useLinkedDoc.ts +226 -13
- package/hooks/useMentionAutocomplete.ts +1 -1
- package/hooks/usePackageChangesSetting.ts +90 -0
- package/hooks/usePlanLookDialog.ts +67 -0
- package/hooks/useSharing.ts +11 -7
- package/hooks/useSkillReferenceAutocomplete.ts +7 -3
- package/hooks/useThemePaletteHalves.ts +37 -0
- package/hooks/useUpdateCheck.ts +22 -4
- package/hooks/useUpdateToast.ts +1 -1
- package/hooks/useVimSelection.ts +9 -0
- package/package.json +23 -22
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +1 -0
- package/shortcuts/code-review/explorer.shortcuts.ts +82 -0
- package/shortcuts/core.ts +63 -4
- package/shortcuts/index.ts +1 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +1 -1
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +1 -0
- package/shortcuts/runtime.ts +14 -6
- package/styles.css +1 -1
- package/theme.css +179 -1
- package/themes/plannotator-graphite.css +66 -0
- package/types.ts +26 -9
- package/utils/agentSwitch.ts +41 -20
- package/utils/agentToolSetting.ts +1 -1
- package/utils/aiChatFormat.ts +23 -7
- package/utils/annotationOwnsHighlight.ts +4 -2
- package/utils/asciidocParser.ts +570 -0
- package/utils/autoUpdateNotice.ts +24 -0
- package/utils/belowTitleSlot.ts +174 -0
- package/utils/codeLineScroll.ts +100 -0
- package/utils/commentSubmitKey.ts +48 -0
- package/utils/cssFontFamily.ts +65 -0
- package/utils/decisionSpec.ts +190 -2
- package/utils/defaultNotesApp.ts +1 -1
- package/utils/diffFonts.ts +4 -1
- package/utils/documentFont.ts +68 -0
- package/utils/documentTextSize.ts +54 -0
- package/utils/fontDetect.ts +63 -0
- package/utils/inboxAnnouncement.ts +78 -0
- package/utils/lookAndFeelAnnouncement.ts +82 -5
- package/utils/markdownExtensions.ts +2 -2
- package/utils/mentions.ts +34 -5
- package/utils/mermaid.ts +20 -3
- package/utils/mermaidTheme.ts +3 -51
- package/utils/notion.ts +58 -0
- package/utils/parser.ts +28 -1
- package/utils/platform.ts +46 -0
- package/utils/refCard.ts +82 -0
- package/utils/richClipboard.ts +380 -0
- package/utils/richClipboardLists.ts +57 -0
- package/utils/richCopyPolicy.ts +22 -0
- package/utils/scrollAnchor.ts +82 -0
- package/utils/sectionRefs.ts +123 -0
- package/utils/selectionBoundary.ts +40 -0
- package/utils/sendFailure.ts +85 -0
- package/utils/sharing.ts +5 -2
- package/utils/themeRegistry.ts +10 -0
- package/utils/themeTokens.ts +74 -0
- package/utils/updateNotice.ts +30 -8
- package/utils/vscodePanel.ts +9 -0
package/components/Settings.tsx
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import type { UpdateCheckSetting } from '../hooks/useUpdateCheckSetting';
|
|
2
|
+
import { cookieHostDescription, type CookieHostSetting } from '../hooks/useCookieHostSetting';
|
|
3
|
+
import type { PackageChangesSetting } from '../hooks/usePackageChangesSetting';
|
|
2
4
|
import React, { useState, useEffect, useMemo, useRef } from 'react';
|
|
3
5
|
import { createPortal } from 'react-dom';
|
|
4
6
|
import type { AnnotateAgentTerminalSide } from '@plannotator/core/agent-terminal';
|
|
5
7
|
import type { Origin } from '@plannotator/core/agents';
|
|
6
8
|
import type { DiffLineBgIntensity } from '@plannotator/core/config-types';
|
|
7
9
|
import type { TokenHoverDelay } from '@plannotator/core/token-hover';
|
|
8
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
configStore,
|
|
12
|
+
useConfigValue,
|
|
13
|
+
setReviewPanelView,
|
|
14
|
+
setReviewDefaultDiffType,
|
|
15
|
+
getProviderReviewDefaultDiffType,
|
|
16
|
+
setProviderReviewDefaultDiffType,
|
|
17
|
+
setReviewAutoViewed,
|
|
18
|
+
type VcsReviewSettingsDescriptor,
|
|
19
|
+
} from '../config';
|
|
9
20
|
import { setWebMcpToolsEnabled, useWebMcpToolsEnabled } from '../webmcp/preference';
|
|
10
21
|
import { DIAGRAM_SHADOW_OPTIONS } from '../utils/diagramShadow';
|
|
22
|
+
import { markLookAndFeelChoiceResolved } from '../utils/lookAndFeelAnnouncement';
|
|
23
|
+
import { DEFAULT_DOCUMENT_TEXT_SIZE, DOCUMENT_TEXT_SIZE_OPTIONS } from '../utils/documentTextSize';
|
|
24
|
+
import { DOCUMENT_FONT_OPTIONS, documentFontStack } from '../utils/documentFont';
|
|
11
25
|
import {
|
|
12
26
|
agentToolAppliesWhen,
|
|
13
27
|
agentToolCostNote,
|
|
@@ -15,6 +29,8 @@ import {
|
|
|
15
29
|
type AgentToolSetting,
|
|
16
30
|
} from '../utils/agentToolSetting';
|
|
17
31
|
import { loadDiffFont } from '../utils/diffFonts';
|
|
32
|
+
import { isFontInstalled } from '../utils/fontDetect';
|
|
33
|
+
import { MAX_FONT_FAMILY_LENGTH, monoFontStack, normalizeCssFontSize, normalizeFontFamilyName } from '../utils/cssFontFamily';
|
|
18
34
|
import { TaterSpritePullup } from './TaterSpritePullup';
|
|
19
35
|
import { getIdentity, regenerateIdentity, setCustomIdentity, isIdentityEditable } from '../utils/identity';
|
|
20
36
|
import { GitUser } from '../icons/GitUser';
|
|
@@ -37,6 +53,13 @@ import {
|
|
|
37
53
|
saveOctarineSettings,
|
|
38
54
|
type OctarineSettings,
|
|
39
55
|
} from '../utils/octarine';
|
|
56
|
+
import {
|
|
57
|
+
getNotionSettings,
|
|
58
|
+
saveNotionSettings,
|
|
59
|
+
getNotionParentPageId,
|
|
60
|
+
isNotionAvailable,
|
|
61
|
+
type NotionSettings,
|
|
62
|
+
} from '../utils/notion';
|
|
40
63
|
import {
|
|
41
64
|
getAgentSwitchSettings,
|
|
42
65
|
getAgentSwitchDefaults,
|
|
@@ -86,7 +109,7 @@ import {
|
|
|
86
109
|
import { requestVimDocumentFocus } from '../hooks/useVimDocumentFocus';
|
|
87
110
|
import { AnalysisLayerToggle } from './AnalysisLayerToggle';
|
|
88
111
|
|
|
89
|
-
type SettingsTab = 'general' | 'theme' | '
|
|
112
|
+
type SettingsTab = 'general' | 'theme' | 'review' | 'display' | 'analysis' | 'saving' | 'labels' | 'vim' | 'shortcuts' | 'ai' | 'files' | 'obsidian' | 'bear' | 'octarine' | 'notion' | 'comments' | 'hooks' | 'experimental';
|
|
90
113
|
|
|
91
114
|
interface SettingsProps {
|
|
92
115
|
taterMode: boolean;
|
|
@@ -101,7 +124,30 @@ interface SettingsProps {
|
|
|
101
124
|
* true; a host that omits it keeps the pre-parity annotate tab set.
|
|
102
125
|
*/
|
|
103
126
|
annotateParity?: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Hide General's "Enter saves a comment" row (#1934). Default false: the
|
|
129
|
+
* row shows. A host that drives the `enterSavesComment` setting itself, or
|
|
130
|
+
* never wants it offered, passes true.
|
|
131
|
+
*/
|
|
132
|
+
hideEnterSavesComment?: boolean;
|
|
133
|
+
/**
|
|
134
|
+
* Show Display's Text size row (#2066). Default false: the row does
|
|
135
|
+
* something only where the page applies the `documentTextSize` setting
|
|
136
|
+
* (`hooks/useDocumentTextSize`), so a host opts in. Plannotator passes true
|
|
137
|
+
* in plan review and annotate on a markdown or text document.
|
|
138
|
+
*/
|
|
139
|
+
showDocumentTextSize?: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Show Display's Document font row (#1657). Default false, like
|
|
142
|
+
* `showDocumentTextSize`: the row does something only where the page
|
|
143
|
+
* applies the `documentFontFamily` setting (`hooks/useDocumentFont`).
|
|
144
|
+
*/
|
|
145
|
+
showDocumentFont?: boolean;
|
|
104
146
|
onUIPreferencesChange?: (prefs: UIPreferences) => void;
|
|
147
|
+
/** Claude Code's permission mode was changed here (plan review). The host
|
|
148
|
+
* keeps its own copy in step, since its Approve sends the mode and its
|
|
149
|
+
* decision menu shows it. Optional: hosts that omit it are unchanged. */
|
|
150
|
+
onPermissionModeChange?: (mode: PermissionMode) => void;
|
|
105
151
|
/** Externally controlled open state (for mobile menu integration) */
|
|
106
152
|
externalOpen?: boolean;
|
|
107
153
|
onExternalClose?: () => void;
|
|
@@ -117,6 +163,11 @@ interface SettingsProps {
|
|
|
117
163
|
* useUpdateCheckSetting); `env` is PLANNOTATOR_UPDATE_CHECK when it overrides the
|
|
118
164
|
* config file. Hosts that omit it get no row. */
|
|
119
165
|
updateCheckSetting?: UpdateCheckSetting;
|
|
166
|
+
/** "Open Plannotator at plannotator.localhost" (#2184, useCookieHostSetting): present
|
|
167
|
+
* only when the server reports it (a local session). Hosts that omit it get no row. */
|
|
168
|
+
cookieHostSetting?: CookieHostSetting;
|
|
169
|
+
/** Code review's package changes switches (usePackageChangesSetting). Hosts that omit it get no rows. */
|
|
170
|
+
packageChangesSetting?: PackageChangesSetting;
|
|
120
171
|
/** The `plannotator` agent tool switch, as the server reports it
|
|
121
172
|
* (parseAgentToolSetting of serverConfig: present only when the session's
|
|
122
173
|
* host registers the tool). Shown only together with onAgentToolChange;
|
|
@@ -126,8 +177,8 @@ interface SettingsProps {
|
|
|
126
177
|
* readable Error, which the row shows. */
|
|
127
178
|
onAgentToolChange?: (enabled: boolean) => Promise<void>;
|
|
128
179
|
/** Current session is a local git review where since-base ISN'T offered
|
|
129
|
-
* (base ref unresolvable) — the
|
|
130
|
-
* preference can't take effect in THIS repo. */
|
|
180
|
+
* (base ref unresolvable) — the Review tab's Git section shows a note that
|
|
181
|
+
* the Git-status preference can't take effect in THIS repo. */
|
|
131
182
|
sinceBaseUnavailable?: boolean;
|
|
132
183
|
/** The host is rendering its compact touch shell (review only). Display
|
|
133
184
|
* settings that the compact shell overrides for the session are hidden
|
|
@@ -213,7 +264,20 @@ const DEFAULT_DIFF_TYPE_OPTIONS = [
|
|
|
213
264
|
{ value: 'merge-base' as const, label: 'Committed changes (PR view)', description: "Everything you've committed on this branch" },
|
|
214
265
|
{ value: 'all' as const, label: 'All Files (HEAD)', description: "Every tracked file at HEAD, shown as additions" },
|
|
215
266
|
];
|
|
216
|
-
|
|
267
|
+
// Labels match the jj diff picker (getJjContext in packages/shared/jj-core.ts).
|
|
268
|
+
const JJ_DEFAULT_DIFF_TYPE_OPTIONS = [
|
|
269
|
+
{ value: 'jj-current', label: 'Current change', description: 'Only the working-copy change (@)' },
|
|
270
|
+
{ value: 'jj-last', label: 'Last change', description: 'The change just before the working copy (@-)' },
|
|
271
|
+
{ value: 'jj-line', label: 'Line of work', description: 'Every change on your line of work since it left trunk' },
|
|
272
|
+
{ value: 'jj-evolog', label: 'Evolution diff', description: 'How the current change has evolved' },
|
|
273
|
+
{ value: 'jj-all', label: 'Every file', description: 'Every file at the working copy, shown as additions' },
|
|
274
|
+
] as const;
|
|
275
|
+
const JJ_REVIEW_DEFAULTS: VcsReviewSettingsDescriptor = {
|
|
276
|
+
id: 'jj',
|
|
277
|
+
label: 'Jujutsu',
|
|
278
|
+
defaultDiffType: 'jj-current',
|
|
279
|
+
diffOptions: JJ_DEFAULT_DIFF_TYPE_OPTIONS.map(({ value, label, description }) => ({ id: value, label, description })),
|
|
280
|
+
};
|
|
217
281
|
const AGENT_TERMINAL_SIDE_OPTIONS: { value: AnnotateAgentTerminalSide; label: string }[] = [
|
|
218
282
|
{ value: 'left', label: 'Left' },
|
|
219
283
|
{ value: 'right', label: 'Right' },
|
|
@@ -249,6 +313,13 @@ function SegmentedControl<T extends string>({ options, value, onChange, disabled
|
|
|
249
313
|
);
|
|
250
314
|
}
|
|
251
315
|
|
|
316
|
+
/** Which of the cookie switch's states the row shows (a test and screenshot hook). */
|
|
317
|
+
function cookieHostSettingState(setting: CookieHostSetting): string {
|
|
318
|
+
if (setting.env !== undefined) return 'locked-env';
|
|
319
|
+
if (setting.urlHost !== undefined) return 'locked-url-host';
|
|
320
|
+
return setting.on ? 'on' : 'off';
|
|
321
|
+
}
|
|
322
|
+
|
|
252
323
|
function ToggleSwitch({ checked, onChange, label, description, disabled = false }: {
|
|
253
324
|
checked: boolean;
|
|
254
325
|
onChange: (v: boolean) => void;
|
|
@@ -417,7 +488,7 @@ function VimSettingsTab({
|
|
|
417
488
|
);
|
|
418
489
|
}
|
|
419
490
|
|
|
420
|
-
function ReviewAnalysisTab() {
|
|
491
|
+
function ReviewAnalysisTab({ packageChangesSetting }: { packageChangesSetting?: PackageChangesSetting }) {
|
|
421
492
|
const semanticDiffEnabled = useConfigValue('semanticDiffEnabled');
|
|
422
493
|
const callFlowEnabled = useConfigValue('callFlowEnabled');
|
|
423
494
|
return (
|
|
@@ -443,6 +514,27 @@ function ReviewAnalysisTab() {
|
|
|
443
514
|
label="Call flow"
|
|
444
515
|
description="Diffs for function call stacks across git commits. 22 languages supported (AST-based, built using Tree-sitter)."
|
|
445
516
|
/>
|
|
517
|
+
{packageChangesSetting && (
|
|
518
|
+
<>
|
|
519
|
+
<div className="border-t border-border" />
|
|
520
|
+
<AnalysisLayerToggle
|
|
521
|
+
checked={packageChangesSetting.card}
|
|
522
|
+
onChange={(on) => packageChangesSetting.setCard(on)}
|
|
523
|
+
label="Package changes"
|
|
524
|
+
description="Added, removed and changed dependencies above package.json and lockfile diffs."
|
|
525
|
+
/>
|
|
526
|
+
<div className="border-t border-border" />
|
|
527
|
+
<AnalysisLayerToggle
|
|
528
|
+
checked={packageChangesSetting.lookup}
|
|
529
|
+
onChange={(on) => void packageChangesSetting.setLookup(on)}
|
|
530
|
+
disabled={packageChangesSetting.lookupEnv === false}
|
|
531
|
+
label="Look up packages on npm"
|
|
532
|
+
description={packageChangesSetting.lookupEnv === false
|
|
533
|
+
? 'Off: PLANNOTATOR_PACKAGE_LOOKUP=0.'
|
|
534
|
+
: 'License, release date and weekly downloads. Sends the names of changed public packages to registry.npmjs.org and api.npmjs.org.'}
|
|
535
|
+
/>
|
|
536
|
+
</>
|
|
537
|
+
)}
|
|
446
538
|
</div>
|
|
447
539
|
|
|
448
540
|
<div className="grid gap-3 sm:grid-cols-2">
|
|
@@ -474,16 +566,56 @@ function ReviewAnalysisTab() {
|
|
|
474
566
|
);
|
|
475
567
|
}
|
|
476
568
|
|
|
477
|
-
|
|
569
|
+
/** One radio card list for a saved default diff (Git or Jujutsu). */
|
|
570
|
+
function DefaultDiffChoices<T extends string>({ options, value, onChange }: {
|
|
571
|
+
options: ReadonlyArray<{ value: T; label: string; description: string }>;
|
|
572
|
+
value: string;
|
|
573
|
+
onChange: (value: T) => void;
|
|
574
|
+
}) {
|
|
575
|
+
return (
|
|
576
|
+
<div className="space-y-2" role="radiogroup">
|
|
577
|
+
{options.map((opt) => (
|
|
578
|
+
<button
|
|
579
|
+
key={opt.value}
|
|
580
|
+
type="button"
|
|
581
|
+
role="radio"
|
|
582
|
+
aria-checked={value === opt.value}
|
|
583
|
+
onClick={() => onChange(opt.value)}
|
|
584
|
+
className={`w-full flex items-start gap-3 p-3 rounded-lg border transition-colors text-left ${
|
|
585
|
+
value === opt.value
|
|
586
|
+
? 'border-primary bg-primary/5'
|
|
587
|
+
: 'border-border hover:border-muted-foreground/30 hover:bg-muted/50'
|
|
588
|
+
}`}
|
|
589
|
+
>
|
|
590
|
+
<div className={`mt-0.5 w-4 h-4 rounded-full border-2 flex-shrink-0 flex items-center justify-center ${
|
|
591
|
+
value === opt.value ? 'border-primary' : 'border-muted-foreground/40'
|
|
592
|
+
}`}>
|
|
593
|
+
{value === opt.value && (
|
|
594
|
+
<div className="w-2 h-2 rounded-full bg-primary" />
|
|
595
|
+
)}
|
|
596
|
+
</div>
|
|
597
|
+
<div>
|
|
598
|
+
<div className="text-sm font-medium">{opt.label}</div>
|
|
599
|
+
<div className="text-xs text-muted-foreground">{opt.description}</div>
|
|
600
|
+
</div>
|
|
601
|
+
</button>
|
|
602
|
+
))}
|
|
603
|
+
</div>
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const REVIEW_SECTION_HEADING = 'text-xs font-semibold uppercase tracking-wider text-muted-foreground';
|
|
608
|
+
|
|
609
|
+
const ReviewTab: React.FC<{ sinceBaseUnavailable?: boolean }> = ({ sinceBaseUnavailable }) => {
|
|
478
610
|
const defaultDiffType = useConfigValue('defaultDiffType');
|
|
611
|
+
useConfigValue('reviewDefaults');
|
|
612
|
+
const jjDefaultDiffType = getProviderReviewDefaultDiffType(JJ_REVIEW_DEFAULTS);
|
|
479
613
|
const reviewPanelView = useConfigValue('reviewPanelView');
|
|
480
614
|
const reviewAutoViewed = useConfigValue('reviewAutoViewed');
|
|
481
615
|
return (
|
|
482
616
|
<div className="space-y-5">
|
|
483
617
|
<div className="space-y-2">
|
|
484
|
-
<div className=
|
|
485
|
-
Viewed files
|
|
486
|
-
</div>
|
|
618
|
+
<div className={REVIEW_SECTION_HEADING}>Viewed files</div>
|
|
487
619
|
{/* Never write `reviewAutoViewed` directly — setReviewAutoViewed also
|
|
488
620
|
consumes the first-time notice, since an explicit toggle is proof
|
|
489
621
|
the reviewer already found the switch. */}
|
|
@@ -494,67 +626,253 @@ const GitTab: React.FC<{ sinceBaseUnavailable?: boolean }> = ({ sinceBaseUnavail
|
|
|
494
626
|
description="Mark a file viewed when you scroll past it or move on to another file. Files you un-view stay un-viewed, and files that change on refresh become un-viewed."
|
|
495
627
|
/>
|
|
496
628
|
</div>
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
629
|
+
|
|
630
|
+
<div className="border-t border-border" />
|
|
631
|
+
|
|
632
|
+
<section className="space-y-4" aria-labelledby="settings-review-git">
|
|
633
|
+
<div id="settings-review-git" className={REVIEW_SECTION_HEADING}>Git</div>
|
|
634
|
+
<div className="space-y-2">
|
|
635
|
+
<div>
|
|
636
|
+
<div className="text-sm font-medium">Default review view</div>
|
|
637
|
+
<div className="text-xs text-muted-foreground">Which panel a code review opens in</div>
|
|
638
|
+
{/* This is a GLOBAL preference — never hide the options because the
|
|
639
|
+
CURRENT repo can't serve them; just say so. Without this note,
|
|
640
|
+
picking Git status on a repo whose base ref doesn't resolve
|
|
641
|
+
silently falls back to Tree and the setting looks broken. */}
|
|
642
|
+
{sinceBaseUnavailable && (
|
|
643
|
+
<div className="text-xs text-warning mt-1">
|
|
644
|
+
Git status view isn't available in this repository (its base branch
|
|
645
|
+
couldn't be resolved) — reviews here open in Tree. The preference
|
|
646
|
+
still applies in repositories where it works.
|
|
647
|
+
</div>
|
|
648
|
+
)}
|
|
649
|
+
</div>
|
|
650
|
+
{/* No Commits option here: the Commits view is session-only (entered
|
|
651
|
+
via the panel toggle) and is never the opening view. */}
|
|
652
|
+
<SegmentedControl
|
|
653
|
+
options={[
|
|
654
|
+
{ value: 'sections' as const, label: 'Git status' },
|
|
655
|
+
{ value: 'tree' as const, label: 'Tree' },
|
|
656
|
+
]}
|
|
657
|
+
value={reviewPanelView}
|
|
658
|
+
onChange={setReviewPanelView}
|
|
659
|
+
/>
|
|
512
660
|
</div>
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
661
|
+
<div className="space-y-2">
|
|
662
|
+
<div>
|
|
663
|
+
<div className="text-sm font-medium">Default diff view</div>
|
|
664
|
+
<div className="text-xs text-muted-foreground">Which changes to show when you open a code review</div>
|
|
665
|
+
</div>
|
|
666
|
+
{/* Coupling (sections ⟺ since-base) lives in the shared setter —
|
|
667
|
+
never write the pair by hand (see config/reviewView). */}
|
|
668
|
+
<DefaultDiffChoices
|
|
669
|
+
options={DEFAULT_DIFF_TYPE_OPTIONS}
|
|
670
|
+
value={defaultDiffType}
|
|
671
|
+
onChange={(value) => setReviewDefaultDiffType(value)}
|
|
672
|
+
/>
|
|
673
|
+
</div>
|
|
674
|
+
</section>
|
|
675
|
+
|
|
676
|
+
<div className="border-t border-border" />
|
|
677
|
+
|
|
678
|
+
<section className="space-y-4" aria-labelledby="settings-review-jj">
|
|
679
|
+
<div id="settings-review-jj" className={REVIEW_SECTION_HEADING}>Jujutsu</div>
|
|
680
|
+
<div className="space-y-2">
|
|
681
|
+
<div>
|
|
682
|
+
<div className="text-sm font-medium">Default diff view</div>
|
|
683
|
+
<div className="text-xs text-muted-foreground">Which changes to show when you open a review in a Jujutsu repository</div>
|
|
684
|
+
</div>
|
|
685
|
+
{/* Its own saved value: Git modes mean nothing in a jj repo, and jj has
|
|
686
|
+
no Git status view, so nothing here is coupled to the Git half. */}
|
|
687
|
+
<DefaultDiffChoices
|
|
688
|
+
options={JJ_DEFAULT_DIFF_TYPE_OPTIONS}
|
|
689
|
+
value={jjDefaultDiffType}
|
|
690
|
+
onChange={(value) => setProviderReviewDefaultDiffType('jj', value)}
|
|
691
|
+
/>
|
|
692
|
+
</div>
|
|
693
|
+
</section>
|
|
694
|
+
</div>
|
|
695
|
+
);
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
/** Sentinel for the "Custom…" select option — never persisted as a font name. */
|
|
699
|
+
const CUSTOM_FONT_SENTINEL = '__custom__';
|
|
700
|
+
|
|
701
|
+
/** Inline style rendering a control in the chosen font (quoted and escaped: free text). */
|
|
702
|
+
function stackStyle(stack: string): React.CSSProperties | undefined {
|
|
703
|
+
return stack ? { fontFamily: stack } : undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* A font row (#851, shared by Code Font and Document font): a select of the
|
|
708
|
+
* built-in choices plus "Custom…", which opens a field for the family name of
|
|
709
|
+
* any font installed on this machine. The typed name is saved after a short
|
|
710
|
+
* pause (configStore.set writes the cookie and may POST /api/config), flushed
|
|
711
|
+
* on unmount, and dropped when a built-in is picked mid-pause.
|
|
712
|
+
*/
|
|
713
|
+
const FontFamilyField: React.FC<{
|
|
714
|
+
label: string;
|
|
715
|
+
description: string;
|
|
716
|
+
value: string;
|
|
717
|
+
onSave: (value: string) => void;
|
|
718
|
+
options: readonly { value: string; label: string }[];
|
|
719
|
+
/** The `font-family` the controls preview a value in. */
|
|
720
|
+
previewStack: (value: string) => string;
|
|
721
|
+
previewText: string;
|
|
722
|
+
customAriaLabel: string;
|
|
723
|
+
customPlaceholder: string;
|
|
724
|
+
notFoundText: string;
|
|
725
|
+
/** Load a built-in's web font for the preview, if it has one. */
|
|
726
|
+
loadFont?: (value: string) => void;
|
|
727
|
+
previewClassName?: string;
|
|
728
|
+
}> = ({ label, description, value, onSave, options, previewStack, previewText, customAriaLabel, customPlaceholder, notFoundText, loadFont, previewClassName = '' }) => {
|
|
729
|
+
const isBuiltIn = (v: string) => options.some((opt) => opt.value === v);
|
|
730
|
+
// Custom font: progressive disclosure. customFontMode keeps the input open
|
|
731
|
+
// while the user picks "Custom…" or has a non-built-in font saved.
|
|
732
|
+
const [customFontMode, setCustomFontMode] = useState(() => !!value && !isBuiltIn(value));
|
|
733
|
+
const [customFontDraft, setCustomFontDraft] = useState(() => (value && !isBuiltIn(value) ? value : ''));
|
|
734
|
+
const customFontSaveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
|
735
|
+
// Pending debounced value. Non-null means "typed but not yet saved" — it is
|
|
736
|
+
// flushed (not discarded) on unmount, and cancelled when a built-in is
|
|
737
|
+
// picked so a stale timer can't overwrite the new selection.
|
|
738
|
+
const pendingFontSave = useRef<string | null>(null);
|
|
739
|
+
const onSaveRef = useRef(onSave);
|
|
740
|
+
onSaveRef.current = onSave;
|
|
741
|
+
const customActive = customFontMode || (!!value && !isBuiltIn(value));
|
|
742
|
+
// Width-measurement check — deterministic per machine, no permissions.
|
|
743
|
+
const customFontInstalled = useMemo(
|
|
744
|
+
() => (normalizeFontFamilyName(customFontDraft) ? isFontInstalled(customFontDraft) : null),
|
|
745
|
+
[customFontDraft],
|
|
746
|
+
);
|
|
747
|
+
|
|
748
|
+
// Load font for the preview swatch
|
|
749
|
+
useEffect(() => {
|
|
750
|
+
if (value) loadFont?.(value);
|
|
751
|
+
}, [value, loadFont]);
|
|
752
|
+
|
|
753
|
+
// Adopt external changes (late server hydration, edits from elsewhere) into
|
|
754
|
+
// the draft — but never clobber typing that hasn't been saved yet.
|
|
755
|
+
useEffect(() => {
|
|
756
|
+
if (pendingFontSave.current !== null) return;
|
|
757
|
+
if (value && !isBuiltIn(value) && value !== normalizeFontFamilyName(customFontDraft)) {
|
|
758
|
+
setCustomFontDraft(value);
|
|
759
|
+
}
|
|
760
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
761
|
+
}, [value]);
|
|
762
|
+
|
|
763
|
+
const cancelPendingFontSave = () => {
|
|
764
|
+
clearTimeout(customFontSaveTimer.current);
|
|
765
|
+
pendingFontSave.current = null;
|
|
766
|
+
};
|
|
767
|
+
|
|
768
|
+
// Flush a pending save on unmount so closing the panel mid-debounce
|
|
769
|
+
// doesn't lose the typed font.
|
|
770
|
+
useEffect(() => () => {
|
|
771
|
+
clearTimeout(customFontSaveTimer.current);
|
|
772
|
+
if (pendingFontSave.current !== null) {
|
|
773
|
+
onSaveRef.current(pendingFontSave.current);
|
|
774
|
+
}
|
|
775
|
+
}, []);
|
|
776
|
+
|
|
777
|
+
const handleCustomFontInput = (next: string) => {
|
|
778
|
+
setCustomFontDraft(next);
|
|
779
|
+
// Debounce the save — per-keystroke writes would spam the cookie and
|
|
780
|
+
// /api/config.
|
|
781
|
+
clearTimeout(customFontSaveTimer.current);
|
|
782
|
+
pendingFontSave.current = normalizeFontFamilyName(next);
|
|
783
|
+
customFontSaveTimer.current = setTimeout(() => {
|
|
784
|
+
if (pendingFontSave.current !== null) {
|
|
785
|
+
onSaveRef.current(pendingFontSave.current);
|
|
786
|
+
pendingFontSave.current = null;
|
|
787
|
+
}
|
|
788
|
+
}, 400);
|
|
789
|
+
};
|
|
790
|
+
|
|
791
|
+
return (
|
|
792
|
+
<div className="space-y-2">
|
|
525
793
|
<div>
|
|
526
|
-
<div className="text-sm font-medium">
|
|
527
|
-
<div className="text-xs text-muted-foreground">
|
|
794
|
+
<div className="text-sm font-medium">{label}</div>
|
|
795
|
+
<div className="text-xs text-muted-foreground">{description}</div>
|
|
528
796
|
</div>
|
|
529
|
-
<
|
|
530
|
-
{
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
</div>
|
|
550
|
-
<div>
|
|
551
|
-
<div className="text-sm font-medium">{opt.label}</div>
|
|
552
|
-
<div className="text-xs text-muted-foreground">{opt.description}</div>
|
|
553
|
-
</div>
|
|
554
|
-
</button>
|
|
797
|
+
<select
|
|
798
|
+
value={customActive ? CUSTOM_FONT_SENTINEL : value}
|
|
799
|
+
aria-label={label}
|
|
800
|
+
onChange={(e) => {
|
|
801
|
+
if (e.target.value === CUSTOM_FONT_SENTINEL) {
|
|
802
|
+
setCustomFontMode(true);
|
|
803
|
+
} else {
|
|
804
|
+
// Cancel any in-flight debounced custom save so a stale timer
|
|
805
|
+
// can't overwrite this selection.
|
|
806
|
+
cancelPendingFontSave();
|
|
807
|
+
setCustomFontMode(false);
|
|
808
|
+
setCustomFontDraft('');
|
|
809
|
+
onSave(e.target.value);
|
|
810
|
+
}
|
|
811
|
+
}}
|
|
812
|
+
className="w-full px-3 py-1.5 text-sm rounded-md bg-muted/50 border border-border text-foreground"
|
|
813
|
+
style={value && !customActive ? stackStyle(previewStack(value)) : undefined}
|
|
814
|
+
>
|
|
815
|
+
{options.map((opt) => (
|
|
816
|
+
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
555
817
|
))}
|
|
818
|
+
<option value={CUSTOM_FONT_SENTINEL}>Custom…</option>
|
|
819
|
+
</select>
|
|
820
|
+
{customActive && (
|
|
821
|
+
<div className="space-y-1">
|
|
822
|
+
<input
|
|
823
|
+
type="text"
|
|
824
|
+
value={customFontDraft}
|
|
825
|
+
onChange={(e) => handleCustomFontInput(e.target.value)}
|
|
826
|
+
placeholder={customPlaceholder}
|
|
827
|
+
aria-label={customAriaLabel}
|
|
828
|
+
maxLength={MAX_FONT_FAMILY_LENGTH}
|
|
829
|
+
autoFocus={!customFontDraft}
|
|
830
|
+
spellCheck={false}
|
|
831
|
+
className="w-full px-3 py-1.5 text-sm rounded-md bg-muted/50 border border-border text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-ring"
|
|
832
|
+
style={stackStyle(previewStack(customFontDraft))}
|
|
833
|
+
/>
|
|
834
|
+
<div className="text-[11px]" aria-live="polite">
|
|
835
|
+
{!normalizeFontFamilyName(customFontDraft) ? (
|
|
836
|
+
<span className="text-muted-foreground">Any font installed on this machine works — type its family name.</span>
|
|
837
|
+
) : customFontInstalled === true ? (
|
|
838
|
+
<span className="text-success">✓ Found on this machine</span>
|
|
839
|
+
) : customFontInstalled === false ? (
|
|
840
|
+
<span className="text-warning">{notFoundText}</span>
|
|
841
|
+
) : null}
|
|
842
|
+
</div>
|
|
843
|
+
</div>
|
|
844
|
+
)}
|
|
845
|
+
{value && (
|
|
846
|
+
<div
|
|
847
|
+
className={`text-xs text-muted-foreground px-1 py-1 rounded bg-muted/30 ${previewClassName}`}
|
|
848
|
+
style={stackStyle(previewStack(value))}
|
|
849
|
+
>
|
|
850
|
+
{previewText}
|
|
851
|
+
</div>
|
|
852
|
+
)}
|
|
853
|
+
</div>
|
|
854
|
+
);
|
|
855
|
+
};
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Experimental (the owner, 2026-10-27: "its own page on settings", the last
|
|
859
|
+
* tab): each experimental setting, off by default. Today one, code review's
|
|
860
|
+
* Edit Code to Suggest (`editSuggestions`, the
|
|
861
|
+
* `plannotator-experimental-edit-suggestions` cookie, unchanged).
|
|
862
|
+
*/
|
|
863
|
+
const ExperimentalTab: React.FC = () => {
|
|
864
|
+
const editSuggestions = useConfigValue('editSuggestions');
|
|
865
|
+
return (
|
|
866
|
+
<div className="space-y-3" data-settings-experimental="">
|
|
867
|
+
<div className="text-xs text-muted-foreground">
|
|
868
|
+
Features still being tried. Each stays off until you turn it on.
|
|
556
869
|
</div>
|
|
557
|
-
|
|
870
|
+
<ToggleSwitch
|
|
871
|
+
checked={editSuggestions}
|
|
872
|
+
onChange={(v) => configStore.set('editSuggestions', v)}
|
|
873
|
+
label="Edit Code to Suggest"
|
|
874
|
+
description="Edit a file in place in the all-files view; your net change becomes a suggestion comment. Files are never written from the browser. Uses an experimental upstream editor."
|
|
875
|
+
/>
|
|
558
876
|
</div>
|
|
559
877
|
);
|
|
560
878
|
};
|
|
@@ -568,35 +886,14 @@ const ReviewDisplayTab: React.FC<{ isCompactTouchLayout?: boolean }> = ({ isComp
|
|
|
568
886
|
const diffShowBackground = useConfigValue('diffShowBackground');
|
|
569
887
|
const diffLineBgIntensity = useConfigValue('diffLineBgIntensity');
|
|
570
888
|
const diffHideWhitespace = useConfigValue('diffHideWhitespace');
|
|
571
|
-
const editSuggestions = useConfigValue('editSuggestions');
|
|
572
889
|
const diffExpandUnchanged = useConfigValue('diffExpandUnchanged');
|
|
573
890
|
const diffFontFamily = useConfigValue('diffFontFamily');
|
|
574
891
|
const diffFontSize = useConfigValue('diffFontSize');
|
|
575
892
|
const tokenHoverTrigger = useConfigValue('tokenHoverTrigger');
|
|
576
893
|
const tokenHoverDelay = useConfigValue('tokenHoverDelay');
|
|
577
894
|
|
|
578
|
-
// Load font for the preview swatch
|
|
579
|
-
useEffect(() => {
|
|
580
|
-
if (diffFontFamily) loadDiffFont(diffFontFamily);
|
|
581
|
-
}, [diffFontFamily]);
|
|
582
|
-
|
|
583
895
|
return (
|
|
584
896
|
<>
|
|
585
|
-
{/* Experimental: edit code to author suggestions */}
|
|
586
|
-
<div className="space-y-3">
|
|
587
|
-
<div className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
|
588
|
-
Experimental
|
|
589
|
-
</div>
|
|
590
|
-
<ToggleSwitch
|
|
591
|
-
checked={editSuggestions}
|
|
592
|
-
onChange={(v) => configStore.set('editSuggestions', v)}
|
|
593
|
-
label="Edit Code to Suggest"
|
|
594
|
-
description="Edit a file in place in the all-files view; your net change becomes a suggestion comment. Files are never written from the browser. Uses an experimental upstream editor."
|
|
595
|
-
/>
|
|
596
|
-
</div>
|
|
597
|
-
|
|
598
|
-
<div className="border-t border-border" />
|
|
599
|
-
|
|
600
897
|
{/* Hover cards (internally tokenHover*; the label is what changed, not
|
|
601
898
|
the ids). One trigger select rather than a toggle plus a mode: `Off`
|
|
602
899
|
is a value of the same question, so there is no unreachable
|
|
@@ -633,30 +930,20 @@ const ReviewDisplayTab: React.FC<{ isCompactTouchLayout?: boolean }> = ({ isComp
|
|
|
633
930
|
<div className="border-t border-border" />
|
|
634
931
|
|
|
635
932
|
{/* Font Family */}
|
|
636
|
-
<
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
</select>
|
|
651
|
-
{diffFontFamily && (
|
|
652
|
-
<div
|
|
653
|
-
className="text-xs text-muted-foreground px-1 py-1 rounded bg-muted/30 font-mono"
|
|
654
|
-
style={{ fontFamily: `'${diffFontFamily}', monospace` }}
|
|
655
|
-
>
|
|
656
|
-
Preview: const x = fn(42);
|
|
657
|
-
</div>
|
|
658
|
-
)}
|
|
659
|
-
</div>
|
|
933
|
+
<FontFamilyField
|
|
934
|
+
label="Code Font"
|
|
935
|
+
description="Font family for diff code lines"
|
|
936
|
+
value={diffFontFamily}
|
|
937
|
+
onSave={(v) => configStore.set('diffFontFamily', v)}
|
|
938
|
+
options={DIFF_FONT_OPTIONS}
|
|
939
|
+
previewStack={monoFontStack}
|
|
940
|
+
previewText="Preview: const x = fn(42);"
|
|
941
|
+
customAriaLabel="Custom code font name"
|
|
942
|
+
customPlaceholder="Font name as installed, e.g. Iosevka"
|
|
943
|
+
notFoundText="Not found — diffs will fall back to monospace"
|
|
944
|
+
loadFont={loadDiffFont}
|
|
945
|
+
previewClassName="font-mono"
|
|
946
|
+
/>
|
|
660
947
|
|
|
661
948
|
<div className="border-t border-border" />
|
|
662
949
|
|
|
@@ -668,7 +955,7 @@ const ReviewDisplayTab: React.FC<{ isCompactTouchLayout?: boolean }> = ({ isComp
|
|
|
668
955
|
<div className="text-xs text-muted-foreground">Font size for diff code lines</div>
|
|
669
956
|
</div>
|
|
670
957
|
<div className="text-xs tabular-nums text-muted-foreground min-w-[4ch] text-right">
|
|
671
|
-
{diffFontSize || 'Auto'}
|
|
958
|
+
{normalizeCssFontSize(diffFontSize) || 'Auto'}
|
|
672
959
|
</div>
|
|
673
960
|
</div>
|
|
674
961
|
<div className="flex items-center gap-2">
|
|
@@ -1007,7 +1294,7 @@ const CommentsTab: React.FC = () => {
|
|
|
1007
1294
|
);
|
|
1008
1295
|
};
|
|
1009
1296
|
|
|
1010
|
-
export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange, onIdentityChange, origin, mode = 'plan', annotateParity = false, onUIPreferencesChange, externalOpen, onExternalClose, aiProviders = [], gitUser, autoUpdateSetting, updateCheckSetting, agentToolSetting, onAgentToolChange, sinceBaseUnavailable, isCompactTouchLayout = false, onDetectObsidianVaults, agentTerminalAvailable = false, webmcpAvailable = false }) => {
|
|
1297
|
+
export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange, onIdentityChange, origin, mode = 'plan', annotateParity = false, onUIPreferencesChange, onPermissionModeChange, externalOpen, onExternalClose, aiProviders = [], gitUser, autoUpdateSetting, updateCheckSetting, cookieHostSetting, packageChangesSetting, agentToolSetting, onAgentToolChange, sinceBaseUnavailable, isCompactTouchLayout = false, onDetectObsidianVaults, agentTerminalAvailable = false, webmcpAvailable = false, hideEnterSavesComment = false, showDocumentTextSize = false, showDocumentFont = false }) => {
|
|
1011
1298
|
const webmcpTools = useWebMcpToolsEnabled();
|
|
1012
1299
|
const autoUpdate = useConfigValue('autoUpdate');
|
|
1013
1300
|
const updateChecksOn = updateCheckSetting ? updateCheckSetting.on : true;
|
|
@@ -1040,7 +1327,10 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1040
1327
|
const [activeTab, setActiveTab] = useState<SettingsTab>('general');
|
|
1041
1328
|
const gridEnabled = useConfigValue('gridEnabled');
|
|
1042
1329
|
const diagramShadow = useConfigValue('diagramShadow');
|
|
1330
|
+
const documentTextSize = useConfigValue('documentTextSize');
|
|
1331
|
+
const documentFontFamily = useConfigValue('documentFontFamily');
|
|
1043
1332
|
const vimModeEnabled = useConfigValue('vimModeEnabled');
|
|
1333
|
+
const enterSavesComment = useConfigValue('enterSavesComment');
|
|
1044
1334
|
const vimHudEnabled = useConfigValue('vimHudEnabled');
|
|
1045
1335
|
const vimHudKeyPanelEnabled = useConfigValue('vimHudKeyPanelEnabled');
|
|
1046
1336
|
const agentTerminalSide = useConfigValue('agentTerminalSide');
|
|
@@ -1057,6 +1347,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1057
1347
|
const [vaultsLoading, setVaultsLoading] = useState(false);
|
|
1058
1348
|
const [bear, setBear] = useState<BearSettings>({ enabled: false, customTags: '', tagPosition: 'append', autoSave: false });
|
|
1059
1349
|
const [octarine, setOctarine] = useState<OctarineSettings>({ enabled: false, workspace: '', folder: 'plannotator', autoSave: false });
|
|
1350
|
+
const [notion, setNotion] = useState<NotionSettings>({ enabled: false, parentPageId: '', autoSave: false });
|
|
1060
1351
|
// The agent switch default depends on the surface: plan approval hands off to
|
|
1061
1352
|
// the build agent, review feedback stays on the current agent.
|
|
1062
1353
|
const agentSurface: AgentSwitchSurface = mode === 'review' ? 'review' : 'plan';
|
|
@@ -1089,7 +1380,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1089
1380
|
t.push({ id: 'labels', label: 'Labels' });
|
|
1090
1381
|
}
|
|
1091
1382
|
if (mode === 'review') {
|
|
1092
|
-
t.push({ id: '
|
|
1383
|
+
t.push({ id: 'review', label: 'Review' });
|
|
1093
1384
|
t.push({ id: 'display', label: 'Editor' });
|
|
1094
1385
|
t.push({ id: 'analysis', label: 'Analysis' });
|
|
1095
1386
|
t.push({ id: 'comments', label: 'Comments' });
|
|
@@ -1110,12 +1401,16 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1110
1401
|
const integrationTabs: { id: SettingsTab; label: string }[] = [
|
|
1111
1402
|
{ id: 'files', label: 'Files' },
|
|
1112
1403
|
...(mode === 'plan' || annotateDocumentTabs
|
|
1113
|
-
? [{ id: 'obsidian' as SettingsTab, label: 'Obsidian' }, { id: 'bear' as SettingsTab, label: 'Bear' }, { id: 'octarine' as SettingsTab, label: 'Octarine' }]
|
|
1404
|
+
? [{ id: 'obsidian' as SettingsTab, label: 'Obsidian' }, { id: 'bear' as SettingsTab, label: 'Bear' }, { id: 'octarine' as SettingsTab, label: 'Octarine' }, ...(isNotionAvailable() ? [{ id: 'notion' as SettingsTab, label: 'Notion' }] : [])]
|
|
1114
1405
|
: []),
|
|
1115
1406
|
];
|
|
1407
|
+
// Experimental is the last tab, on its own below the integrations (the owner,
|
|
1408
|
+
// 2026-10-27). Only where it has a setting: code review's Edit Code to Suggest.
|
|
1409
|
+
const experimentalTabs: { id: SettingsTab; label: string }[] = mode === 'review' ? [{ id: 'experimental', label: 'Experimental' }] : [];
|
|
1116
1410
|
const obsidianDefaultSaveAvailable = obsidian.enabled && getEffectiveVaultPath(obsidian).trim().length > 0;
|
|
1117
1411
|
const bearDefaultSaveAvailable = bear.enabled;
|
|
1118
1412
|
const octarineDefaultSaveAvailable = octarine.enabled && octarine.workspace.trim().length > 0;
|
|
1413
|
+
const notionDefaultSaveAvailable = getNotionParentPageId(notion) !== null;
|
|
1119
1414
|
|
|
1120
1415
|
// Sync external open state
|
|
1121
1416
|
useEffect(() => {
|
|
@@ -1131,6 +1426,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1131
1426
|
setObsidian(getObsidianSettings());
|
|
1132
1427
|
setBear(getBearSettings());
|
|
1133
1428
|
setOctarine(getOctarineSettings());
|
|
1429
|
+
setNotion(getNotionSettings());
|
|
1134
1430
|
setAgent(getAgentSwitchSettings(agentSurface));
|
|
1135
1431
|
setPlanSave(getPlanSaveSettings());
|
|
1136
1432
|
setUiPrefs(getUIPreferences());
|
|
@@ -1156,7 +1452,9 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1156
1452
|
defaultNotesApp === 'download' ||
|
|
1157
1453
|
(defaultNotesApp === 'obsidian' && obsidianDefaultSaveAvailable) ||
|
|
1158
1454
|
(defaultNotesApp === 'bear' && bearDefaultSaveAvailable) ||
|
|
1159
|
-
(defaultNotesApp === 'octarine' && octarineDefaultSaveAvailable)
|
|
1455
|
+
(defaultNotesApp === 'octarine' && octarineDefaultSaveAvailable) ||
|
|
1456
|
+
// A session without Notion (remote, or no NOTION_TOKEN) keeps the choice for the next one.
|
|
1457
|
+
(defaultNotesApp === 'notion' && (notionDefaultSaveAvailable || !isNotionAvailable()));
|
|
1160
1458
|
|
|
1161
1459
|
if (!defaultSaveAvailable) {
|
|
1162
1460
|
setDefaultNotesApp('ask');
|
|
@@ -1168,6 +1466,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1168
1466
|
obsidianDefaultSaveAvailable,
|
|
1169
1467
|
bearDefaultSaveAvailable,
|
|
1170
1468
|
octarineDefaultSaveAvailable,
|
|
1469
|
+
notionDefaultSaveAvailable,
|
|
1171
1470
|
]);
|
|
1172
1471
|
|
|
1173
1472
|
// Fetch detected vaults when Obsidian is enabled
|
|
@@ -1224,6 +1523,12 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1224
1523
|
saveOctarineSettings(newSettings);
|
|
1225
1524
|
};
|
|
1226
1525
|
|
|
1526
|
+
const handleNotionChange = (updates: Partial<NotionSettings>) => {
|
|
1527
|
+
const newSettings = { ...notion, ...updates };
|
|
1528
|
+
setNotion(newSettings);
|
|
1529
|
+
saveNotionSettings(newSettings);
|
|
1530
|
+
};
|
|
1531
|
+
|
|
1227
1532
|
const handleAgentChange = (switchTo: AgentSwitchSettings['switchTo'], customName?: string) => {
|
|
1228
1533
|
const newSettings = { switchTo, customName: customName ?? agent.customName };
|
|
1229
1534
|
setAgent(newSettings);
|
|
@@ -1246,6 +1551,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1246
1551
|
const handlePermissionModeChange = (mode: PermissionMode) => {
|
|
1247
1552
|
setPermissionMode(mode);
|
|
1248
1553
|
savePermissionModeSettings(mode);
|
|
1554
|
+
onPermissionModeChange?.(mode);
|
|
1249
1555
|
};
|
|
1250
1556
|
|
|
1251
1557
|
const handleDefaultNotesAppChange = (app: DefaultNotesApp) => {
|
|
@@ -1331,7 +1637,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1331
1637
|
<div className="flex flex-col md:flex-row md:min-h-[420px] flex-1 min-h-0 overflow-hidden">
|
|
1332
1638
|
{/* Mobile: horizontal tab bar */}
|
|
1333
1639
|
<nav className="md:hidden flex overflow-x-auto border-b border-border px-2 py-1.5 gap-1 flex-shrink-0">
|
|
1334
|
-
{[...mainTabs, ...integrationTabs].map(tab => (
|
|
1640
|
+
{[...mainTabs, ...integrationTabs, ...experimentalTabs].map(tab => (
|
|
1335
1641
|
<button
|
|
1336
1642
|
key={tab.id}
|
|
1337
1643
|
onClick={() => setActiveTab(tab.id)}
|
|
@@ -1386,6 +1692,26 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1386
1692
|
</div>
|
|
1387
1693
|
</>
|
|
1388
1694
|
)}
|
|
1695
|
+
{experimentalTabs.length > 0 && (
|
|
1696
|
+
<>
|
|
1697
|
+
<div className="mx-2 my-2 border-t border-border/50" />
|
|
1698
|
+
<div className="space-y-0.5">
|
|
1699
|
+
{experimentalTabs.map(tab => (
|
|
1700
|
+
<button
|
|
1701
|
+
key={tab.id}
|
|
1702
|
+
onClick={() => setActiveTab(tab.id)}
|
|
1703
|
+
className={`w-full text-left px-3 py-1.5 rounded text-sm transition-colors ${
|
|
1704
|
+
activeTab === tab.id
|
|
1705
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
1706
|
+
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
|
1707
|
+
}`}
|
|
1708
|
+
>
|
|
1709
|
+
{tab.label}
|
|
1710
|
+
</button>
|
|
1711
|
+
))}
|
|
1712
|
+
</div>
|
|
1713
|
+
</>
|
|
1714
|
+
)}
|
|
1389
1715
|
</nav>
|
|
1390
1716
|
|
|
1391
1717
|
{/* Content — scrollable */}
|
|
@@ -1447,6 +1773,22 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1447
1773
|
</div>
|
|
1448
1774
|
</div>
|
|
1449
1775
|
|
|
1776
|
+
{/* Enter saves a comment (#1934): opt-in, cookie-only.
|
|
1777
|
+
Governs every comment composer; never the decision keys. */}
|
|
1778
|
+
{!hideEnterSavesComment && (
|
|
1779
|
+
<>
|
|
1780
|
+
<div className="border-t border-border" />
|
|
1781
|
+
<div data-enter-saves-comment-setting="true">
|
|
1782
|
+
<ToggleSwitch
|
|
1783
|
+
checked={enterSavesComment}
|
|
1784
|
+
onChange={(v) => configStore.set('enterSavesComment', v)}
|
|
1785
|
+
label="Enter saves a comment"
|
|
1786
|
+
description={`Shift+Enter adds a new line. ${modKeyWord}+Enter still saves.`}
|
|
1787
|
+
/>
|
|
1788
|
+
</div>
|
|
1789
|
+
</>
|
|
1790
|
+
)}
|
|
1791
|
+
|
|
1450
1792
|
{/* Update notices: on by default. Writes updateCheck to
|
|
1451
1793
|
~/.plannotator/config.json; off makes no request to GitHub. */}
|
|
1452
1794
|
{updateCheckSetting && (
|
|
@@ -1460,7 +1802,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1460
1802
|
description={
|
|
1461
1803
|
updateCheckSetting.env !== undefined
|
|
1462
1804
|
? 'Set by PLANNOTATOR_UPDATE_CHECK in your environment.'
|
|
1463
|
-
: 'This computer asks GitHub
|
|
1805
|
+
: 'This computer asks GitHub every few minutes, never your browser. Off makes no request at all.'
|
|
1464
1806
|
}
|
|
1465
1807
|
/>
|
|
1466
1808
|
</>
|
|
@@ -1488,6 +1830,28 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1488
1830
|
</>
|
|
1489
1831
|
)}
|
|
1490
1832
|
|
|
1833
|
+
{/* Open Plannotator at plannotator.localhost (#2184): writes or
|
|
1834
|
+
removes urlHost "plannotator.localhost" in config.json.
|
|
1835
|
+
Locked while PLANNOTATOR_URL_HOST is set or config.json
|
|
1836
|
+
holds another urlHost, which it never replaces. */}
|
|
1837
|
+
{cookieHostSetting && (
|
|
1838
|
+
<>
|
|
1839
|
+
<div className="border-t border-border" />
|
|
1840
|
+
<div data-cookie-host-setting={cookieHostSettingState(cookieHostSetting)}>
|
|
1841
|
+
<ToggleSwitch
|
|
1842
|
+
checked={cookieHostSetting.on}
|
|
1843
|
+
onChange={(v) => cookieHostSetting.onChange(v)}
|
|
1844
|
+
disabled={cookieHostSetting.env !== undefined || cookieHostSetting.urlHost !== undefined}
|
|
1845
|
+
label="Open Plannotator at plannotator.localhost"
|
|
1846
|
+
description={cookieHostDescription(cookieHostSetting)}
|
|
1847
|
+
/>
|
|
1848
|
+
{cookieHostSetting.error && (
|
|
1849
|
+
<div role="alert" className="mt-1 text-xs text-destructive">{cookieHostSetting.error}</div>
|
|
1850
|
+
)}
|
|
1851
|
+
</div>
|
|
1852
|
+
</>
|
|
1853
|
+
)}
|
|
1854
|
+
|
|
1491
1855
|
{/* The `plannotator` agent tool switch: only where the
|
|
1492
1856
|
server reports a tool host (Claude Code mod, Pi,
|
|
1493
1857
|
OpenCode 2). Writes agentTool to config.json; applies
|
|
@@ -1709,9 +2073,9 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1709
2073
|
{/* === THEME TAB === */}
|
|
1710
2074
|
{activeTab === 'theme' && <ThemeTab onPreview={() => { setShowDialog(false); setThemePreview(true); }} />}
|
|
1711
2075
|
|
|
1712
|
-
{/* ===
|
|
1713
|
-
{activeTab === '
|
|
1714
|
-
<
|
|
2076
|
+
{/* === REVIEW TAB === */}
|
|
2077
|
+
{activeTab === 'review' && mode === 'review' && (
|
|
2078
|
+
<ReviewTab sinceBaseUnavailable={sinceBaseUnavailable} />
|
|
1715
2079
|
)}
|
|
1716
2080
|
|
|
1717
2081
|
{/* === DISPLAY TAB === */}
|
|
@@ -1719,12 +2083,64 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1719
2083
|
<ReviewDisplayTab isCompactTouchLayout={isCompactTouchLayout} />
|
|
1720
2084
|
)}
|
|
1721
2085
|
|
|
2086
|
+
{activeTab === 'experimental' && mode === 'review' && <ExperimentalTab />}
|
|
2087
|
+
|
|
1722
2088
|
{activeTab === 'analysis' && mode === 'review' && (
|
|
1723
|
-
<ReviewAnalysisTab />
|
|
2089
|
+
<ReviewAnalysisTab packageChangesSetting={packageChangesSetting} />
|
|
1724
2090
|
)}
|
|
1725
2091
|
|
|
1726
2092
|
{activeTab === 'display' && mode !== 'review' && (
|
|
1727
2093
|
<>
|
|
2094
|
+
{/* Text size (#2066): the document only, not the app around it */}
|
|
2095
|
+
{showDocumentTextSize && (<>
|
|
2096
|
+
<div className="space-y-3" data-document-text-size-setting>
|
|
2097
|
+
<div>
|
|
2098
|
+
<div className="text-sm font-medium" id="document-text-size-label">Text size</div>
|
|
2099
|
+
<div className="text-xs text-muted-foreground">
|
|
2100
|
+
{mode === 'annotate' ? 'Body text of the document' : 'Body text of the plan'}, in px ({DEFAULT_DOCUMENT_TEXT_SIZE} is the default)
|
|
2101
|
+
</div>
|
|
2102
|
+
</div>
|
|
2103
|
+
<div className="flex items-center gap-1 bg-muted/50 rounded-lg p-0.5" role="group" aria-labelledby="document-text-size-label">
|
|
2104
|
+
{DOCUMENT_TEXT_SIZE_OPTIONS.map((value) => (
|
|
2105
|
+
<button
|
|
2106
|
+
key={value}
|
|
2107
|
+
onClick={() => configStore.set('documentTextSize', value)}
|
|
2108
|
+
aria-pressed={documentTextSize === value}
|
|
2109
|
+
className={`flex-1 px-3 py-1.5 text-xs rounded-md transition-colors tabular-nums ${
|
|
2110
|
+
documentTextSize === value
|
|
2111
|
+
? 'bg-background text-foreground shadow-sm font-medium'
|
|
2112
|
+
: 'text-muted-foreground hover:text-foreground'
|
|
2113
|
+
}`}
|
|
2114
|
+
>
|
|
2115
|
+
{value}
|
|
2116
|
+
</button>
|
|
2117
|
+
))}
|
|
2118
|
+
</div>
|
|
2119
|
+
</div>
|
|
2120
|
+
|
|
2121
|
+
<div className="border-t border-border" />
|
|
2122
|
+
</>)}
|
|
2123
|
+
|
|
2124
|
+
{/* Document font (#1657): the document only; code keeps its font */}
|
|
2125
|
+
{showDocumentFont && (<>
|
|
2126
|
+
<div data-document-font-setting>
|
|
2127
|
+
<FontFamilyField
|
|
2128
|
+
label="Document font"
|
|
2129
|
+
description={`${mode === 'annotate' ? 'Text of the document' : 'Text of the plan'}; code keeps its monospace font`}
|
|
2130
|
+
value={documentFontFamily}
|
|
2131
|
+
onSave={(v) => configStore.set('documentFontFamily', v)}
|
|
2132
|
+
options={DOCUMENT_FONT_OPTIONS}
|
|
2133
|
+
previewStack={documentFontStack}
|
|
2134
|
+
previewText="Preview: The quick brown fox jumps over the lazy dog."
|
|
2135
|
+
customAriaLabel="Custom document font name"
|
|
2136
|
+
customPlaceholder="Font name as installed, e.g. Iowan Old Style"
|
|
2137
|
+
notFoundText="Not found — the document keeps the theme font"
|
|
2138
|
+
/>
|
|
2139
|
+
</div>
|
|
2140
|
+
|
|
2141
|
+
<div className="border-t border-border" />
|
|
2142
|
+
</>)}
|
|
2143
|
+
|
|
1728
2144
|
{/* Auto-open Sidebar */}
|
|
1729
2145
|
<div className="flex items-center justify-between">
|
|
1730
2146
|
<div>
|
|
@@ -1783,8 +2199,12 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1783
2199
|
<div className="text-sm font-medium">Grid Background</div>
|
|
1784
2200
|
<div className="text-xs text-muted-foreground">Show the plan as a floating card on a grid</div>
|
|
1785
2201
|
</div>
|
|
1786
|
-
<button role="switch" aria-checked={gridEnabled}
|
|
1787
|
-
onClick={() =>
|
|
2202
|
+
<button role="switch" aria-checked={gridEnabled} aria-label="Grid Background"
|
|
2203
|
+
onClick={() => {
|
|
2204
|
+
configStore.set('gridEnabled', !gridEnabled);
|
|
2205
|
+
// A Settings flip is a real choice: keep it across reloads.
|
|
2206
|
+
markLookAndFeelChoiceResolved();
|
|
2207
|
+
}}
|
|
1788
2208
|
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${gridEnabled ? 'bg-primary' : 'bg-muted'}`}>
|
|
1789
2209
|
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${gridEnabled ? 'translate-x-6' : 'translate-x-1'}`} />
|
|
1790
2210
|
</button>
|
|
@@ -2014,13 +2434,14 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
2014
2434
|
{obsidianDefaultSaveAvailable && <option value="obsidian">Obsidian</option>}
|
|
2015
2435
|
{bearDefaultSaveAvailable && <option value="bear">Bear</option>}
|
|
2016
2436
|
{octarineDefaultSaveAvailable && <option value="octarine">Octarine</option>}
|
|
2437
|
+
{(notionDefaultSaveAvailable || defaultNotesApp === 'notion') && <option value="notion">Notion</option>}
|
|
2017
2438
|
</select>
|
|
2018
2439
|
<div className="text-[10px] text-muted-foreground/70">
|
|
2019
2440
|
{defaultNotesApp === 'ask'
|
|
2020
2441
|
? 'Opens Export dialog with Notes tab'
|
|
2021
2442
|
: defaultNotesApp === 'download'
|
|
2022
2443
|
? `${modKey}+S downloads the annotations file`
|
|
2023
|
-
: `${modKey}+S saves directly to ${{ obsidian: 'Obsidian', bear: 'Bear', octarine: 'Octarine' }[defaultNotesApp] ?? defaultNotesApp}`}
|
|
2444
|
+
: `${modKey}+S saves directly to ${{ obsidian: 'Obsidian', bear: 'Bear', octarine: 'Octarine', notion: 'Notion' }[defaultNotesApp] ?? defaultNotesApp}`}
|
|
2024
2445
|
</div>
|
|
2025
2446
|
</div>
|
|
2026
2447
|
|
|
@@ -2156,7 +2577,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
2156
2577
|
onClick={() => handleFileBrowserChange({
|
|
2157
2578
|
directories: fileBrowserSettings.directories.filter((d) => d !== dir),
|
|
2158
2579
|
})}
|
|
2159
|
-
className="p-1.5 rounded text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors opacity-0 group-hover:opacity-100"
|
|
2580
|
+
className="p-1.5 rounded text-muted-foreground hover:text-destructive hover:bg-destructive/10 transition-colors opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 [@media(hover:none)]:opacity-100"
|
|
2160
2581
|
title="Remove directory"
|
|
2161
2582
|
>
|
|
2162
2583
|
<svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
@@ -2574,6 +2995,76 @@ tags: [${mode === 'annotate' ? 'plannotator' : 'plan'}, ...]
|
|
|
2574
2995
|
</>
|
|
2575
2996
|
)}
|
|
2576
2997
|
|
|
2998
|
+
{/* === NOTION TAB === */}
|
|
2999
|
+
{activeTab === 'notion' && (
|
|
3000
|
+
<>
|
|
3001
|
+
<div className="flex items-center justify-between">
|
|
3002
|
+
<div>
|
|
3003
|
+
<div className="text-sm font-medium">Notion</div>
|
|
3004
|
+
<div className="text-xs text-muted-foreground">
|
|
3005
|
+
{mode === 'annotate' ? 'Save documents to Notion from the Options menu. When on, plan review also saves each approved plan here.' : 'Save approved plans as Notion pages'}
|
|
3006
|
+
</div>
|
|
3007
|
+
</div>
|
|
3008
|
+
<button
|
|
3009
|
+
role="switch"
|
|
3010
|
+
aria-checked={notion.enabled}
|
|
3011
|
+
onClick={() => handleNotionChange({ enabled: !notion.enabled })}
|
|
3012
|
+
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
|
3013
|
+
notion.enabled ? 'bg-primary' : 'bg-muted'
|
|
3014
|
+
}`}
|
|
3015
|
+
>
|
|
3016
|
+
<span
|
|
3017
|
+
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
|
|
3018
|
+
notion.enabled ? 'translate-x-6' : 'translate-x-1'
|
|
3019
|
+
}`}
|
|
3020
|
+
/>
|
|
3021
|
+
</button>
|
|
3022
|
+
</div>
|
|
3023
|
+
{notion.enabled && (
|
|
3024
|
+
<div className="mt-3 space-y-3">
|
|
3025
|
+
<div className="space-y-1.5 pl-0.5">
|
|
3026
|
+
<label className="text-xs text-muted-foreground">Parent Page</label>
|
|
3027
|
+
<input
|
|
3028
|
+
type="text"
|
|
3029
|
+
value={notion.parentPageId}
|
|
3030
|
+
onChange={(e) => handleNotionChange({ parentPageId: e.target.value })}
|
|
3031
|
+
placeholder="https://www.notion.so/..."
|
|
3032
|
+
className="w-full px-3 py-2 bg-muted rounded-lg text-xs font-mono placeholder:text-muted-foreground/50 focus:outline-none focus:ring-1 focus:ring-primary/50"
|
|
3033
|
+
/>
|
|
3034
|
+
<div className="text-[10px] text-muted-foreground">
|
|
3035
|
+
A page link or ID. Each save adds a page under it. Share it with your Notion integration first.
|
|
3036
|
+
</div>
|
|
3037
|
+
</div>
|
|
3038
|
+
|
|
3039
|
+
{mode === 'plan' && (<>
|
|
3040
|
+
<div className="border-t border-border/30" />
|
|
3041
|
+
|
|
3042
|
+
<div className="flex items-center justify-between">
|
|
3043
|
+
<div>
|
|
3044
|
+
<div className="text-xs font-medium">Auto-save on Plan Arrival</div>
|
|
3045
|
+
<div className="text-[10px] text-muted-foreground">
|
|
3046
|
+
Automatically save to Notion when a plan loads, before you approve or deny
|
|
3047
|
+
</div>
|
|
3048
|
+
</div>
|
|
3049
|
+
<button
|
|
3050
|
+
role="switch"
|
|
3051
|
+
aria-checked={notion.autoSave}
|
|
3052
|
+
onClick={() => handleNotionChange({ autoSave: !notion.autoSave })}
|
|
3053
|
+
className={`relative inline-flex h-6 w-11 flex-shrink-0 items-center rounded-full transition-colors ${
|
|
3054
|
+
notion.autoSave ? 'bg-primary' : 'bg-muted'
|
|
3055
|
+
}`}
|
|
3056
|
+
>
|
|
3057
|
+
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow-sm transition-transform ${
|
|
3058
|
+
notion.autoSave ? 'translate-x-6' : 'translate-x-1'
|
|
3059
|
+
}`} />
|
|
3060
|
+
</button>
|
|
3061
|
+
</div>
|
|
3062
|
+
</>)}
|
|
3063
|
+
</div>
|
|
3064
|
+
)}
|
|
3065
|
+
</>
|
|
3066
|
+
)}
|
|
3067
|
+
|
|
2577
3068
|
</div>
|
|
2578
3069
|
</OverlayScrollArea>
|
|
2579
3070
|
</div>
|