@plannotator/ui 0.45.1 → 0.46.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 +51 -1
- package/README.md +1 -1
- package/components/AgentsTab.tsx +108 -108
- package/components/BlockRenderer.tsx +9 -5
- package/components/ExportModal.tsx +8 -4
- package/components/ImageAnnotator/Toolbar.tsx +13 -3
- package/components/ImageAnnotator/index.tsx +6 -2
- package/components/InlineMarkdown.tsx +14 -7
- package/components/RenderedMarkdown.tsx +3 -1
- package/components/Settings.tsx +38 -15
- package/components/Viewer.tsx +76 -18
- package/components/blocks/AlertBlock.tsx +3 -2
- package/components/blocks/Callout.tsx +3 -0
- package/components/blocks/CodeBlock.tsx +6 -2
- package/components/blocks/TableBlock.tsx +4 -0
- package/components/blocks/TablePopout.tsx +6 -2
- package/components/blocks/proseBody.tsx +3 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +6 -2
- package/config/reviewView.ts +8 -3
- package/config/settings.ts +10 -4
- package/hooks/useAgentSettings.ts +108 -123
- package/hooks/useAnnotationHighlighter.ts +109 -1
- package/hooks/useCodeAnnotationDraft.ts +196 -3
- package/hooks/useModelCatalogs.ts +116 -0
- package/package.json +2 -2
- package/styles.css +1 -1
- package/types.ts +31 -0
- package/utils/aiProvider.ts +8 -16
- package/utils/annotateAgentInstructions.ts +162 -0
- package/utils/codeHighlight.ts +18 -0
- package/utils/parser.ts +141 -31
- package/utils/terminalToolsAnnouncement.ts +2 -2
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/utils/codexModels.ts +0 -79
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { useState, useRef, useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { isCodeFilePath, isCodeFilePathStrict, CODE_PATH_BARE_REGEX, parseCodePath } from "@plannotator/core/code-file";
|
|
4
|
+
import { forgeRefLinks } from "@plannotator/core/forge-refs";
|
|
4
5
|
import { ensureHighlight, highlightToHtml } from "../utils/codeHighlight";
|
|
5
6
|
import { useFenceTheme } from "../hooks/useFenceTheme";
|
|
6
7
|
import { transformPlainText } from "../utils/inlineTransforms";
|
|
@@ -501,9 +502,13 @@ export const InlineMarkdown: React.FC<{
|
|
|
501
502
|
onNavigateAnchor?: (hash: string) => void;
|
|
502
503
|
imageBaseDir?: string;
|
|
503
504
|
onImageClick?: (src: string, alt: string) => void;
|
|
505
|
+
/** Repo path (`owner/repo`, `group/sub/project`) bare `#123` / `@user` refs link into. */
|
|
504
506
|
githubRepo?: string;
|
|
505
|
-
|
|
507
|
+
/** Host of that repo's git remote; picks the forge refs link to. Absent means github.com. */
|
|
508
|
+
repoHost?: string;
|
|
509
|
+
}> = ({ text, onOpenLinkedDoc, resolveLinkedDoc, onOpenCodeFile, onNavigateAnchor, imageBaseDir, onImageClick, githubRepo, repoHost }) => {
|
|
506
510
|
const validation = useCodePathValidation();
|
|
511
|
+
const refLinks = forgeRefLinks({ display: githubRepo, host: repoHost });
|
|
507
512
|
const parts: React.ReactNode[] = [];
|
|
508
513
|
let remaining = text;
|
|
509
514
|
let key = 0;
|
|
@@ -672,6 +677,7 @@ export const InlineMarkdown: React.FC<{
|
|
|
672
677
|
onOpenCodeFile={onOpenCodeFile}
|
|
673
678
|
onNavigateAnchor={onNavigateAnchor}
|
|
674
679
|
githubRepo={githubRepo}
|
|
680
|
+
repoHost={repoHost}
|
|
675
681
|
/>
|
|
676
682
|
</del>,
|
|
677
683
|
);
|
|
@@ -695,6 +701,7 @@ export const InlineMarkdown: React.FC<{
|
|
|
695
701
|
onOpenCodeFile={onOpenCodeFile}
|
|
696
702
|
onNavigateAnchor={onNavigateAnchor}
|
|
697
703
|
githubRepo={githubRepo}
|
|
704
|
+
repoHost={repoHost}
|
|
698
705
|
/>
|
|
699
706
|
</em>
|
|
700
707
|
</strong>,
|
|
@@ -718,6 +725,7 @@ export const InlineMarkdown: React.FC<{
|
|
|
718
725
|
onOpenCodeFile={onOpenCodeFile}
|
|
719
726
|
onNavigateAnchor={onNavigateAnchor}
|
|
720
727
|
githubRepo={githubRepo}
|
|
728
|
+
repoHost={repoHost}
|
|
721
729
|
/>
|
|
722
730
|
</strong>,
|
|
723
731
|
);
|
|
@@ -740,6 +748,7 @@ export const InlineMarkdown: React.FC<{
|
|
|
740
748
|
onOpenCodeFile={onOpenCodeFile}
|
|
741
749
|
onNavigateAnchor={onNavigateAnchor}
|
|
742
750
|
githubRepo={githubRepo}
|
|
751
|
+
repoHost={repoHost}
|
|
743
752
|
/>
|
|
744
753
|
</em>,
|
|
745
754
|
);
|
|
@@ -763,6 +772,7 @@ export const InlineMarkdown: React.FC<{
|
|
|
763
772
|
onOpenCodeFile={onOpenCodeFile}
|
|
764
773
|
onNavigateAnchor={onNavigateAnchor}
|
|
765
774
|
githubRepo={githubRepo}
|
|
775
|
+
repoHost={repoHost}
|
|
766
776
|
/>
|
|
767
777
|
</em>,
|
|
768
778
|
);
|
|
@@ -834,9 +844,7 @@ export const InlineMarkdown: React.FC<{
|
|
|
834
844
|
match = remaining.match(/^#(\d+)(?!\w)/);
|
|
835
845
|
if (match) {
|
|
836
846
|
const num = match[1];
|
|
837
|
-
const href =
|
|
838
|
-
? `https://github.com/${githubRepo}/issues/${num}`
|
|
839
|
-
: null;
|
|
847
|
+
const href = refLinks?.issue(num) ?? null;
|
|
840
848
|
const label = `#${num}`;
|
|
841
849
|
parts.push(
|
|
842
850
|
href ? (
|
|
@@ -864,9 +872,7 @@ export const InlineMarkdown: React.FC<{
|
|
|
864
872
|
match = remaining.match(/^@([a-zA-Z][a-zA-Z0-9_-]{0,38})(?!\w)/);
|
|
865
873
|
if (match) {
|
|
866
874
|
const handle = match[1];
|
|
867
|
-
const href =
|
|
868
|
-
? `https://github.com/${handle}`
|
|
869
|
-
: null;
|
|
875
|
+
const href = refLinks?.user(handle) ?? null;
|
|
870
876
|
const label = `@${handle}`;
|
|
871
877
|
parts.push(
|
|
872
878
|
href ? (
|
|
@@ -1140,6 +1146,7 @@ export const InlineMarkdown: React.FC<{
|
|
|
1140
1146
|
onOpenCodeFile={onOpenCodeFile}
|
|
1141
1147
|
onNavigateAnchor={onNavigateAnchor}
|
|
1142
1148
|
githubRepo={githubRepo}
|
|
1149
|
+
repoHost={repoHost}
|
|
1143
1150
|
imageBaseDir={imageBaseDir}
|
|
1144
1151
|
onImageClick={onImageClick}
|
|
1145
1152
|
/>,
|
|
@@ -18,6 +18,7 @@ export interface RenderedMarkdownProps {
|
|
|
18
18
|
onOpenLinkedDoc?: (path: string) => void;
|
|
19
19
|
onOpenCodeFile?: (path: string) => void;
|
|
20
20
|
githubRepo?: string;
|
|
21
|
+
repoHost?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
export const RenderedMarkdown: React.FC<RenderedMarkdownProps> = ({
|
|
@@ -27,9 +28,10 @@ export const RenderedMarkdown: React.FC<RenderedMarkdownProps> = ({
|
|
|
27
28
|
onOpenLinkedDoc,
|
|
28
29
|
onOpenCodeFile,
|
|
29
30
|
githubRepo,
|
|
31
|
+
repoHost,
|
|
30
32
|
}) => {
|
|
31
33
|
const groups = useMemo(() => groupBlocks(parseMarkdownToBlocks(markdown)), [markdown]);
|
|
32
|
-
const cb = { onImageClick, onOpenLinkedDoc, onOpenCodeFile, githubRepo };
|
|
34
|
+
const cb = { onImageClick, onOpenLinkedDoc, onOpenCodeFile, githubRepo, repoHost };
|
|
33
35
|
|
|
34
36
|
return (
|
|
35
37
|
<div className={className}>
|
package/components/Settings.tsx
CHANGED
|
@@ -87,6 +87,13 @@ interface SettingsProps {
|
|
|
87
87
|
onIdentityChange?: (oldIdentity: string, newIdentity: string) => void;
|
|
88
88
|
origin?: Origin | null;
|
|
89
89
|
mode?: 'plan' | 'annotate' | 'review';
|
|
90
|
+
/**
|
|
91
|
+
* `mode="annotate"` only: offer the document tabs plan review has (Display,
|
|
92
|
+
* Saving, Labels, and the Obsidian / Bear / Octarine integrations) and hide
|
|
93
|
+
* the plan-only OpenCode agent-switch row. Plannotator's annotate app passes
|
|
94
|
+
* true; a host that omits it keeps the pre-parity annotate tab set.
|
|
95
|
+
*/
|
|
96
|
+
annotateParity?: boolean;
|
|
90
97
|
onUIPreferencesChange?: (prefs: UIPreferences) => void;
|
|
91
98
|
/** Externally controlled open state (for mobile menu integration) */
|
|
92
99
|
externalOpen?: boolean;
|
|
@@ -930,7 +937,7 @@ const CommentsTab: React.FC = () => {
|
|
|
930
937
|
);
|
|
931
938
|
};
|
|
932
939
|
|
|
933
|
-
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 }) => {
|
|
940
|
+
export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange, onIdentityChange, origin, mode = 'plan', annotateParity = false, onUIPreferencesChange, externalOpen, onExternalClose, aiProviders = [], gitUser, sinceBaseUnavailable, isCompactTouchLayout = false, onDetectObsidianVaults, agentTerminalAvailable = false, webmcpAvailable = false }) => {
|
|
934
941
|
const webmcpTools = useWebMcpToolsEnabled();
|
|
935
942
|
const [showDialog, setShowDialog] = useState(false);
|
|
936
943
|
const settingsWasOpenRef = useRef(false);
|
|
@@ -981,6 +988,8 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
981
988
|
// The agent switch default depends on the surface: plan approval hands off to
|
|
982
989
|
// the build agent, review feedback stays on the current agent.
|
|
983
990
|
const agentSurface: AgentSwitchSurface = mode === 'review' ? 'review' : 'plan';
|
|
991
|
+
// Annotate with parity on gets the same document tabs as plan review.
|
|
992
|
+
const annotateDocumentTabs = mode === 'annotate' && annotateParity;
|
|
984
993
|
const [agent, setAgent] = useState<AgentSwitchSettings>(() => getAgentSwitchDefaults(agentSurface));
|
|
985
994
|
const [planSave, setPlanSave] = useState<PlanSaveSettings>({ enabled: true, customPath: null });
|
|
986
995
|
const [uiPrefs, setUiPrefs] = useState<UIPreferences>({ tocEnabled: true, stickyActionsEnabled: true, planWidth: 'compact' });
|
|
@@ -1001,7 +1010,11 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1001
1010
|
const mainTabs = useMemo(() => {
|
|
1002
1011
|
const t: { id: SettingsTab; label: string }[] = [{ id: 'general', label: 'General' }];
|
|
1003
1012
|
t.push({ id: 'theme', label: 'Theme' });
|
|
1004
|
-
|
|
1013
|
+
// Annotate renders the same document viewer as plan review, so the
|
|
1014
|
+
// document-level tabs apply there too. Only rows that describe a plan
|
|
1015
|
+
// decision (plan snapshots, plan-arrival auto-save, plan-time hooks,
|
|
1016
|
+
// post-approval permission mode / agent switch) stay plan-only.
|
|
1017
|
+
if (mode === 'plan' || annotateDocumentTabs) {
|
|
1005
1018
|
t.push({ id: 'display', label: 'Display' });
|
|
1006
1019
|
t.push({ id: 'saving', label: 'Saving' });
|
|
1007
1020
|
t.push({ id: 'labels', label: 'Labels' });
|
|
@@ -1023,11 +1036,11 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1023
1036
|
t.push({ id: 'hooks', label: 'Hooks' });
|
|
1024
1037
|
}
|
|
1025
1038
|
return t;
|
|
1026
|
-
}, [mode, aiProviders.length]);
|
|
1039
|
+
}, [mode, annotateDocumentTabs, aiProviders.length]);
|
|
1027
1040
|
|
|
1028
1041
|
const integrationTabs: { id: SettingsTab; label: string }[] = [
|
|
1029
1042
|
{ id: 'files', label: 'Files' },
|
|
1030
|
-
...(mode === 'plan'
|
|
1043
|
+
...(mode === 'plan' || annotateDocumentTabs
|
|
1031
1044
|
? [{ id: 'obsidian' as SettingsTab, label: 'Obsidian' }, { id: 'bear' as SettingsTab, label: 'Bear' }, { id: 'octarine' as SettingsTab, label: 'Octarine' }]
|
|
1032
1045
|
: []),
|
|
1033
1046
|
];
|
|
@@ -1428,8 +1441,9 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1428
1441
|
</>
|
|
1429
1442
|
)}
|
|
1430
1443
|
|
|
1431
|
-
{/* Agent Switching (OpenCode only)
|
|
1432
|
-
|
|
1444
|
+
{/* Agent Switching (OpenCode only). Applied on plan
|
|
1445
|
+
approval only; annotate decisions never switch agents. */}
|
|
1446
|
+
{origin === 'opencode' && !annotateDocumentTabs && (
|
|
1433
1447
|
<>
|
|
1434
1448
|
<div className="border-t border-border" />
|
|
1435
1449
|
<div className="space-y-2">
|
|
@@ -1689,9 +1703,9 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1689
1703
|
{/* Plan Width */}
|
|
1690
1704
|
<div className="space-y-3">
|
|
1691
1705
|
<div>
|
|
1692
|
-
<div className="text-sm font-medium flex items-center gap-2">Plan Width</div>
|
|
1706
|
+
<div className="text-sm font-medium flex items-center gap-2">{mode === 'annotate' ? 'Document Width' : 'Plan Width'}</div>
|
|
1693
1707
|
<div className="text-xs text-muted-foreground">
|
|
1694
|
-
Maximum width of the plan card
|
|
1708
|
+
{mode === 'annotate' ? 'Maximum width of the document card' : 'Maximum width of the plan card'}
|
|
1695
1709
|
</div>
|
|
1696
1710
|
</div>
|
|
1697
1711
|
<div className="flex items-center gap-1 bg-muted/50 rounded-lg p-0.5">
|
|
@@ -1815,7 +1829,9 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1815
1829
|
{/* === SAVING TAB === */}
|
|
1816
1830
|
{activeTab === 'saving' && (
|
|
1817
1831
|
<>
|
|
1818
|
-
{/* Plan Saving
|
|
1832
|
+
{/* Plan Saving — decision snapshots in plans/ are written
|
|
1833
|
+
on plan approve/deny only. */}
|
|
1834
|
+
{mode === 'plan' && (<>
|
|
1819
1835
|
<div className="space-y-3">
|
|
1820
1836
|
<div className="flex items-center justify-between">
|
|
1821
1837
|
<div>
|
|
@@ -1858,6 +1874,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
1858
1874
|
</div>
|
|
1859
1875
|
|
|
1860
1876
|
<div className="border-t border-border" />
|
|
1877
|
+
</>)}
|
|
1861
1878
|
|
|
1862
1879
|
{/* Default Notes App */}
|
|
1863
1880
|
<div className="space-y-2">
|
|
@@ -2256,7 +2273,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
2256
2273
|
<div>
|
|
2257
2274
|
<div className="text-sm font-medium">Obsidian Integration</div>
|
|
2258
2275
|
<div className="text-xs text-muted-foreground">
|
|
2259
|
-
Auto-save approved plans to your vault
|
|
2276
|
+
{mode === 'annotate' ? 'Save documents to your vault from the Options menu. When on, plan review also saves each approved plan here.' : 'Auto-save approved plans to your vault'}
|
|
2260
2277
|
</div>
|
|
2261
2278
|
</div>
|
|
2262
2279
|
<button
|
|
@@ -2385,7 +2402,7 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
2385
2402
|
</div>
|
|
2386
2403
|
|
|
2387
2404
|
<div className="text-[10px] text-muted-foreground/70">
|
|
2388
|
-
Plans saved to: {obsidian.vaultPath === CUSTOM_PATH_SENTINEL
|
|
2405
|
+
{mode === 'annotate' ? 'Saved to' : 'Plans saved to'}: {obsidian.vaultPath === CUSTOM_PATH_SENTINEL
|
|
2389
2406
|
? (obsidian.customPath || '...')
|
|
2390
2407
|
: (obsidian.vaultPath || '...')}/{obsidian.folder || 'plannotator'}/
|
|
2391
2408
|
</div>
|
|
@@ -2396,13 +2413,14 @@ export const Settings: React.FC<SettingsProps> = ({ taterMode, onTaterModeChange
|
|
|
2396
2413
|
{`---
|
|
2397
2414
|
created: ${new Date().toISOString().slice(0, 19)}Z
|
|
2398
2415
|
source: plannotator
|
|
2399
|
-
tags: [plan, ...]
|
|
2416
|
+
tags: [${mode === 'annotate' ? 'plannotator' : 'plan'}, ...]
|
|
2400
2417
|
---`}
|
|
2401
2418
|
</pre>
|
|
2402
2419
|
</div>
|
|
2403
2420
|
|
|
2404
2421
|
<div className="border-t border-border/30" />
|
|
2405
2422
|
|
|
2423
|
+
{mode === 'plan' && (<>
|
|
2406
2424
|
<div className="flex items-center justify-between">
|
|
2407
2425
|
<div>
|
|
2408
2426
|
<div className="text-xs font-medium">Auto-save on Plan Arrival</div>
|
|
@@ -2423,6 +2441,7 @@ tags: [plan, ...]
|
|
|
2423
2441
|
}`} />
|
|
2424
2442
|
</button>
|
|
2425
2443
|
</div>
|
|
2444
|
+
</>)}
|
|
2426
2445
|
|
|
2427
2446
|
<div className="flex items-center justify-between">
|
|
2428
2447
|
<div>
|
|
@@ -2457,7 +2476,7 @@ tags: [plan, ...]
|
|
|
2457
2476
|
<div>
|
|
2458
2477
|
<div className="text-sm font-medium">Bear Notes</div>
|
|
2459
2478
|
<div className="text-xs text-muted-foreground">
|
|
2460
|
-
Auto-save approved plans to Bear
|
|
2479
|
+
{mode === 'annotate' ? 'Save documents to Bear from the Options menu. When on, plan review also saves each approved plan here.' : 'Auto-save approved plans to Bear'}
|
|
2461
2480
|
</div>
|
|
2462
2481
|
</div>
|
|
2463
2482
|
<button
|
|
@@ -2503,6 +2522,7 @@ tags: [plan, ...]
|
|
|
2503
2522
|
</select>
|
|
2504
2523
|
</div>
|
|
2505
2524
|
|
|
2525
|
+
{mode === 'plan' && (<>
|
|
2506
2526
|
<div className="border-t border-border/30" />
|
|
2507
2527
|
|
|
2508
2528
|
<div className="flex items-center justify-between">
|
|
@@ -2525,6 +2545,7 @@ tags: [plan, ...]
|
|
|
2525
2545
|
}`} />
|
|
2526
2546
|
</button>
|
|
2527
2547
|
</div>
|
|
2548
|
+
</>)}
|
|
2528
2549
|
</div>
|
|
2529
2550
|
)}
|
|
2530
2551
|
</>
|
|
@@ -2537,7 +2558,7 @@ tags: [plan, ...]
|
|
|
2537
2558
|
<div>
|
|
2538
2559
|
<div className="text-sm font-medium">Octarine</div>
|
|
2539
2560
|
<div className="text-xs text-muted-foreground">
|
|
2540
|
-
Auto-save approved plans to Octarine
|
|
2561
|
+
{mode === 'annotate' ? 'Save documents to Octarine from the Options menu. When on, plan review also saves each approved plan here.' : 'Auto-save approved plans to Octarine'}
|
|
2541
2562
|
</div>
|
|
2542
2563
|
</div>
|
|
2543
2564
|
<button
|
|
@@ -2585,9 +2606,10 @@ tags: [plan, ...]
|
|
|
2585
2606
|
</div>
|
|
2586
2607
|
|
|
2587
2608
|
<div className="text-[10px] text-muted-foreground/70">
|
|
2588
|
-
Plans saved to: {octarine.workspace || '...'} / {octarine.folder || 'plannotator'}/
|
|
2609
|
+
{mode === 'annotate' ? 'Saved to' : 'Plans saved to'}: {octarine.workspace || '...'} / {octarine.folder || 'plannotator'}/
|
|
2589
2610
|
</div>
|
|
2590
2611
|
|
|
2612
|
+
{mode === 'plan' && (<>
|
|
2591
2613
|
<div className="border-t border-border/30" />
|
|
2592
2614
|
|
|
2593
2615
|
<div className="flex items-center justify-between">
|
|
@@ -2610,6 +2632,7 @@ tags: [plan, ...]
|
|
|
2610
2632
|
}`} />
|
|
2611
2633
|
</button>
|
|
2612
2634
|
</div>
|
|
2635
|
+
</>)}
|
|
2613
2636
|
</div>
|
|
2614
2637
|
)}
|
|
2615
2638
|
</>
|
package/components/Viewer.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { AnnotationType, type Block, type Annotation, type EditorMode, type Inpu
|
|
|
5
5
|
import { applyHighlight, codeBlockClassName, onCodeHighlightSwap } from '../utils/codeHighlight';
|
|
6
6
|
import { paintCodeBlockMark } from '../utils/codeBlockMark';
|
|
7
7
|
import { useFenceTheme } from '../hooks/useFenceTheme';
|
|
8
|
-
import { computeListIndices, groupBlocks, type Frontmatter } from '../utils/parser';
|
|
8
|
+
import { computeListIndices, groupBlocks, type Frontmatter, type FrontmatterValue } from '../utils/parser';
|
|
9
9
|
import { buildHeadingSlugMap } from '../utils/slugify';
|
|
10
10
|
import { copyTextToClipboard } from '../utils/clipboard';
|
|
11
11
|
import { BlockRenderer } from './BlockRenderer';
|
|
@@ -222,6 +222,76 @@ interface CodeBlockToolbarTarget {
|
|
|
222
222
|
readonly activation: 'pointer' | 'keyboard';
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
// Named type guard so both taken and fallthrough branches narrow.
|
|
226
|
+
function isFrontmatterMap(value: FrontmatterValue): value is { [key: string]: FrontmatterValue } {
|
|
227
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Renders a single frontmatter field or recursive sub-structure.
|
|
232
|
+
*/
|
|
233
|
+
const FrontmatterRow: React.FC<{ field: string; value: FrontmatterValue }> = ({ field, value }) => {
|
|
234
|
+
if (isFrontmatterMap(value)) {
|
|
235
|
+
const subEntries = Object.entries(value);
|
|
236
|
+
return (
|
|
237
|
+
<div className="flex flex-col gap-1.5">
|
|
238
|
+
<span className="font-medium text-muted-foreground">{field}:</span>
|
|
239
|
+
<div className="pl-4 grid gap-1.5">
|
|
240
|
+
{subEntries.map(([k, v]) => (
|
|
241
|
+
<FrontmatterRow key={k} field={k} value={v} />
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (Array.isArray(value)) {
|
|
249
|
+
const isArrayOfMaps = value.some((v) => typeof v === 'object' && v !== null);
|
|
250
|
+
if (isArrayOfMaps) {
|
|
251
|
+
return (
|
|
252
|
+
<div className="flex flex-col gap-1.5">
|
|
253
|
+
<span className="font-medium text-muted-foreground">{field}:</span>
|
|
254
|
+
<div className="pl-4 grid gap-2">
|
|
255
|
+
{value.map((item, i) => (
|
|
256
|
+
<div key={i} className="p-2 bg-muted/40 border border-border/40 rounded grid gap-1.5">
|
|
257
|
+
{isFrontmatterMap(item) ? (
|
|
258
|
+
Object.entries(item).map(([k, v]) => (
|
|
259
|
+
<FrontmatterRow key={k} field={k} value={v} />
|
|
260
|
+
))
|
|
261
|
+
) : (
|
|
262
|
+
<span className="text-foreground">{typeof item === 'string' ? item : String(item)}</span>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
))}
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return (
|
|
272
|
+
<div className="flex gap-2">
|
|
273
|
+
<span className="font-medium text-muted-foreground min-w-[80px]">{field}:</span>
|
|
274
|
+
<span className="text-foreground">
|
|
275
|
+
<span className="flex flex-wrap gap-1">
|
|
276
|
+
{value.map((v, i) => (
|
|
277
|
+
<span key={i} className="px-1.5 py-0.5 bg-primary/10 text-primary rounded text-xs">
|
|
278
|
+
{typeof v === 'string' ? v : String(v)}
|
|
279
|
+
</span>
|
|
280
|
+
))}
|
|
281
|
+
</span>
|
|
282
|
+
</span>
|
|
283
|
+
</div>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<div className="flex gap-2">
|
|
289
|
+
<span className="font-medium text-muted-foreground min-w-[80px]">{field}:</span>
|
|
290
|
+
<span className="text-foreground">{value}</span>
|
|
291
|
+
</div>
|
|
292
|
+
);
|
|
293
|
+
};
|
|
294
|
+
|
|
225
295
|
/**
|
|
226
296
|
* Renders YAML frontmatter as a styled metadata card.
|
|
227
297
|
*/
|
|
@@ -233,22 +303,7 @@ const FrontmatterCard: React.FC<{ frontmatter: Frontmatter }> = ({ frontmatter }
|
|
|
233
303
|
<div className="mt-4 mb-6 p-4 bg-muted/30 border border-border/50 rounded-lg">
|
|
234
304
|
<div className="grid gap-2 text-sm">
|
|
235
305
|
{entries.map(([key, value]) => (
|
|
236
|
-
<
|
|
237
|
-
<span className="font-medium text-muted-foreground min-w-[80px]">{key}:</span>
|
|
238
|
-
<span className="text-foreground">
|
|
239
|
-
{Array.isArray(value) ? (
|
|
240
|
-
<span className="flex flex-wrap gap-1">
|
|
241
|
-
{value.map((v, i) => (
|
|
242
|
-
<span key={i} className="px-1.5 py-0.5 bg-primary/10 text-primary rounded text-xs">
|
|
243
|
-
{v}
|
|
244
|
-
</span>
|
|
245
|
-
))}
|
|
246
|
-
</span>
|
|
247
|
-
) : (
|
|
248
|
-
value
|
|
249
|
-
)}
|
|
250
|
-
</span>
|
|
251
|
-
</div>
|
|
306
|
+
<FrontmatterRow key={key} field={key} value={value} />
|
|
252
307
|
))}
|
|
253
308
|
</div>
|
|
254
309
|
</div>
|
|
@@ -1222,6 +1277,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1222
1277
|
onToggleCheckbox={readOnly ? undefined : onToggleCheckbox}
|
|
1223
1278
|
checkboxOverrides={checkboxOverrides}
|
|
1224
1279
|
githubRepo={repoInfo?.display}
|
|
1280
|
+
repoHost={repoInfo?.host}
|
|
1225
1281
|
headingAnchorId={headingSlugMap.get(block.id)}
|
|
1226
1282
|
onNavigateAnchor={scrollToAnchor}
|
|
1227
1283
|
/>
|
|
@@ -1262,6 +1318,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1262
1318
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
1263
1319
|
onOpenCodeFile={onOpenCodeFile}
|
|
1264
1320
|
githubRepo={repoInfo?.display}
|
|
1321
|
+
repoHost={repoInfo?.host}
|
|
1265
1322
|
onNavigateAnchor={scrollToAnchor}
|
|
1266
1323
|
onHover={(element) => {
|
|
1267
1324
|
if (tableHoverTimeoutRef.current) {
|
|
@@ -1328,7 +1385,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1328
1385
|
}
|
|
1329
1386
|
/>
|
|
1330
1387
|
) : (
|
|
1331
|
-
<BlockRenderer imageBaseDir={imageBaseDir} onImageClick={(src, alt) => setLightbox({ src, alt })} key={group.block.id} block={group.block} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} onNavigateAnchor={scrollToAnchor} onToggleCheckbox={readOnly ? undefined : onToggleCheckbox} checkboxOverrides={checkboxOverrides} githubRepo={repoInfo?.display} headingAnchorId={headingSlugMap.get(group.block.id)} />
|
|
1388
|
+
<BlockRenderer imageBaseDir={imageBaseDir} onImageClick={(src, alt) => setLightbox({ src, alt })} key={group.block.id} block={group.block} onOpenLinkedDoc={onOpenLinkedDoc} onOpenCodeFile={onOpenCodeFile} onNavigateAnchor={scrollToAnchor} onToggleCheckbox={readOnly ? undefined : onToggleCheckbox} checkboxOverrides={checkboxOverrides} githubRepo={repoInfo?.display} repoHost={repoInfo?.host} headingAnchorId={headingSlugMap.get(group.block.id)} />
|
|
1332
1389
|
)
|
|
1333
1390
|
)}
|
|
1334
1391
|
|
|
@@ -1438,6 +1495,7 @@ export const Viewer = forwardRef<ViewerHandle, ViewerProps>(({
|
|
|
1438
1495
|
onOpenLinkedDoc={onOpenLinkedDoc}
|
|
1439
1496
|
onOpenCodeFile={onOpenCodeFile}
|
|
1440
1497
|
githubRepo={repoInfo?.display}
|
|
1498
|
+
repoHost={repoInfo?.host}
|
|
1441
1499
|
onNavigateAnchor={scrollToAnchor}
|
|
1442
1500
|
/>
|
|
1443
1501
|
)}
|
|
@@ -13,6 +13,7 @@ interface AlertBlockProps {
|
|
|
13
13
|
imageBaseDir?: string;
|
|
14
14
|
onImageClick?: (src: string, alt: string) => void;
|
|
15
15
|
githubRepo?: string;
|
|
16
|
+
repoHost?: string;
|
|
16
17
|
onNavigateAnchor?: (hash: string) => void;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -60,9 +61,9 @@ const Icon: React.FC<{ kind: AlertKind }> = ({ kind }) => {
|
|
|
60
61
|
};
|
|
61
62
|
|
|
62
63
|
export const AlertBlock: React.FC<AlertBlockProps> = ({
|
|
63
|
-
blockId, kind, body, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, githubRepo, onNavigateAnchor,
|
|
64
|
+
blockId, kind, body, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, githubRepo, repoHost, onNavigateAnchor,
|
|
64
65
|
}) => {
|
|
65
|
-
const proseProps = { imageBaseDir, onImageClick, onOpenLinkedDoc, onOpenCodeFile, onNavigateAnchor, githubRepo };
|
|
66
|
+
const proseProps = { imageBaseDir, onImageClick, onOpenLinkedDoc, onOpenCodeFile, onNavigateAnchor, githubRepo, repoHost };
|
|
66
67
|
|
|
67
68
|
// A bold-only first line (optionally led by one emoji, optionally trailed by
|
|
68
69
|
// an `<!-- icon: name -->` comment) is the alert's title and rides the icon
|
|
@@ -13,6 +13,7 @@ interface CalloutProps {
|
|
|
13
13
|
imageBaseDir?: string;
|
|
14
14
|
onImageClick?: (src: string, alt: string) => void;
|
|
15
15
|
githubRepo?: string;
|
|
16
|
+
repoHost?: string;
|
|
16
17
|
onNavigateAnchor?: (hash: string) => void;
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -28,6 +29,7 @@ export const Callout: React.FC<CalloutProps> = ({
|
|
|
28
29
|
imageBaseDir,
|
|
29
30
|
onImageClick,
|
|
30
31
|
githubRepo,
|
|
32
|
+
repoHost,
|
|
31
33
|
onNavigateAnchor,
|
|
32
34
|
}) => {
|
|
33
35
|
const kindAttr =
|
|
@@ -54,6 +56,7 @@ export const Callout: React.FC<CalloutProps> = ({
|
|
|
54
56
|
onOpenCodeFile,
|
|
55
57
|
onNavigateAnchor,
|
|
56
58
|
githubRepo,
|
|
59
|
+
repoHost,
|
|
57
60
|
})}
|
|
58
61
|
</div>
|
|
59
62
|
);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
2
2
|
import type { Block } from '../../types';
|
|
3
3
|
import { copyTextToClipboard } from '../../utils/clipboard';
|
|
4
|
-
import { applyHighlight, codeBlockClassName } from '../../utils/codeHighlight';
|
|
4
|
+
import { applyHighlight, codeBlockClassName, codeBlockKeepsLayout } from '../../utils/codeHighlight';
|
|
5
5
|
import { useFenceTheme } from '../../hooks/useFenceTheme';
|
|
6
6
|
|
|
7
7
|
interface CodeBlockProps {
|
|
@@ -68,7 +68,11 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({ block, onHover, onLeave })
|
|
|
68
68
|
)}
|
|
69
69
|
</button>
|
|
70
70
|
<pre className="rounded-lg text-[13px] overflow-x-auto bg-muted/50 border border-border/30">
|
|
71
|
-
<code
|
|
71
|
+
<code
|
|
72
|
+
ref={codeRef}
|
|
73
|
+
className={codeClassName}
|
|
74
|
+
data-keep-layout={codeBlockKeepsLayout(block.content) ? '' : undefined}
|
|
75
|
+
>{block.content}</code>
|
|
72
76
|
</pre>
|
|
73
77
|
</div>
|
|
74
78
|
);
|
|
@@ -12,6 +12,7 @@ interface TableBlockProps {
|
|
|
12
12
|
imageBaseDir?: string;
|
|
13
13
|
onImageClick?: (src: string, alt: string) => void;
|
|
14
14
|
githubRepo?: string;
|
|
15
|
+
repoHost?: string;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
// Parse pipe-delimited markdown table content into headers + rows.
|
|
@@ -87,6 +88,7 @@ export const TableBlock: React.FC<TableBlockProps> = ({
|
|
|
87
88
|
imageBaseDir,
|
|
88
89
|
onImageClick,
|
|
89
90
|
githubRepo,
|
|
91
|
+
repoHost,
|
|
90
92
|
}) => {
|
|
91
93
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
92
94
|
const { headers, rows } = parseTableContent(block.content);
|
|
@@ -116,6 +118,7 @@ export const TableBlock: React.FC<TableBlockProps> = ({
|
|
|
116
118
|
onOpenCodeFile={onOpenCodeFile}
|
|
117
119
|
onNavigateAnchor={onNavigateAnchor}
|
|
118
120
|
githubRepo={githubRepo}
|
|
121
|
+
repoHost={repoHost}
|
|
119
122
|
/>
|
|
120
123
|
</th>
|
|
121
124
|
))}
|
|
@@ -134,6 +137,7 @@ export const TableBlock: React.FC<TableBlockProps> = ({
|
|
|
134
137
|
onOpenCodeFile={onOpenCodeFile}
|
|
135
138
|
onNavigateAnchor={onNavigateAnchor}
|
|
136
139
|
githubRepo={githubRepo}
|
|
140
|
+
repoHost={repoHost}
|
|
137
141
|
/>
|
|
138
142
|
</td>
|
|
139
143
|
))}
|
|
@@ -28,6 +28,7 @@ interface TablePopoutProps {
|
|
|
28
28
|
imageBaseDir?: string;
|
|
29
29
|
onImageClick?: (src: string, alt: string) => void;
|
|
30
30
|
githubRepo?: string;
|
|
31
|
+
repoHost?: string;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
type Row = Record<string, string>;
|
|
@@ -43,6 +44,7 @@ const TablePopoutImpl: React.FC<TablePopoutProps> = ({
|
|
|
43
44
|
imageBaseDir,
|
|
44
45
|
onImageClick,
|
|
45
46
|
githubRepo,
|
|
47
|
+
repoHost,
|
|
46
48
|
}) => {
|
|
47
49
|
const { headers, rows } = useMemo(() => parseTableContent(block.content), [block.content]);
|
|
48
50
|
|
|
@@ -83,11 +85,12 @@ const TablePopoutImpl: React.FC<TablePopoutProps> = ({
|
|
|
83
85
|
onOpenCodeFile={onOpenCodeFile}
|
|
84
86
|
onNavigateAnchor={onNavigateAnchor}
|
|
85
87
|
githubRepo={githubRepo}
|
|
88
|
+
repoHost={repoHost}
|
|
86
89
|
/>
|
|
87
90
|
),
|
|
88
91
|
}),
|
|
89
92
|
);
|
|
90
|
-
}, [columnIds, headers, imageBaseDir, onImageClick, onOpenLinkedDoc, onNavigateAnchor, githubRepo]);
|
|
93
|
+
}, [columnIds, headers, imageBaseDir, onImageClick, onOpenLinkedDoc, onNavigateAnchor, githubRepo, repoHost]);
|
|
91
94
|
|
|
92
95
|
const [sorting, setSorting] = useState<SortingState>([]);
|
|
93
96
|
const [globalFilter, setGlobalFilter] = useState('');
|
|
@@ -263,7 +266,8 @@ export const TablePopout = React.memo(
|
|
|
263
266
|
prev.open === next.open &&
|
|
264
267
|
prev.container === next.container &&
|
|
265
268
|
prev.imageBaseDir === next.imageBaseDir &&
|
|
266
|
-
prev.githubRepo === next.githubRepo
|
|
269
|
+
prev.githubRepo === next.githubRepo &&
|
|
270
|
+
prev.repoHost === next.repoHost,
|
|
267
271
|
);
|
|
268
272
|
|
|
269
273
|
const SortIndicator: React.FC<{ dir: false | 'asc' | 'desc' }> = ({ dir }) => {
|
|
@@ -19,6 +19,7 @@ export function renderProseBody(args: {
|
|
|
19
19
|
onOpenCodeFile?: (path: string) => void;
|
|
20
20
|
onNavigateAnchor?: (hash: string) => void;
|
|
21
21
|
githubRepo?: string;
|
|
22
|
+
repoHost?: string;
|
|
22
23
|
}): React.ReactNode {
|
|
23
24
|
const {
|
|
24
25
|
body,
|
|
@@ -30,6 +31,7 @@ export function renderProseBody(args: {
|
|
|
30
31
|
onOpenCodeFile,
|
|
31
32
|
onNavigateAnchor,
|
|
32
33
|
githubRepo,
|
|
34
|
+
repoHost,
|
|
33
35
|
} = args;
|
|
34
36
|
|
|
35
37
|
const inline = (text: string) => (
|
|
@@ -41,6 +43,7 @@ export function renderProseBody(args: {
|
|
|
41
43
|
onOpenCodeFile={onOpenCodeFile}
|
|
42
44
|
onNavigateAnchor={onNavigateAnchor}
|
|
43
45
|
githubRepo={githubRepo}
|
|
46
|
+
repoHost={repoHost}
|
|
44
47
|
/>
|
|
45
48
|
);
|
|
46
49
|
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import React, { useEffect, useRef, useState, useCallback } from "react";
|
|
10
10
|
import { parseMarkdownToBlocks, computeListIndices } from "../../utils/parser";
|
|
11
|
-
import { applyHighlight, codeBlockClassName } from "../../utils/codeHighlight";
|
|
11
|
+
import { applyHighlight, codeBlockClassName, codeBlockKeepsLayout } from "../../utils/codeHighlight";
|
|
12
12
|
import { useFenceTheme } from "../../hooks/useFenceTheme";
|
|
13
13
|
import { ListItemBody } from "../ListItemBody";
|
|
14
14
|
import type { Block, Annotation, EditorMode, ImageAttachment } from "../../types";
|
|
@@ -711,7 +711,11 @@ const SimpleCodeBlock: React.FC<{ block: Block }> = ({ block }) => {
|
|
|
711
711
|
return (
|
|
712
712
|
<div className="relative group my-5">
|
|
713
713
|
<pre className="bg-muted/50 border border-border/30 rounded-lg overflow-x-auto">
|
|
714
|
-
<code
|
|
714
|
+
<code
|
|
715
|
+
ref={codeRef}
|
|
716
|
+
className={codeBlockClassName(block.language)}
|
|
717
|
+
data-keep-layout={codeBlockKeepsLayout(block.content) ? "" : undefined}
|
|
718
|
+
>
|
|
715
719
|
{block.content}
|
|
716
720
|
</code>
|
|
717
721
|
</pre>
|
package/config/reviewView.ts
CHANGED
|
@@ -20,6 +20,10 @@ import { storage } from '../utils/storage';
|
|
|
20
20
|
* remains the one non-writer that can produce a conflicted pair from a
|
|
21
21
|
* stale config.json; the App-level load reconciler heals that case by
|
|
22
22
|
* calling setReviewPanelView('sections', { recordLastUsed: false }).
|
|
23
|
+
*
|
|
24
|
+
* The registry default is 'tree', which any diff type can render, so the pair
|
|
25
|
+
* only ever needs reconciling for a reviewer who explicitly persisted
|
|
26
|
+
* 'sections'.
|
|
23
27
|
*/
|
|
24
28
|
|
|
25
29
|
/** Store seam for tests (fresh ConfigStoreForTest); production always uses the singleton. */
|
|
@@ -33,7 +37,7 @@ export function setReviewPanelView(
|
|
|
33
37
|
store.set('reviewPanelView', view);
|
|
34
38
|
// An explicit persisted choice also becomes the last-used view — otherwise
|
|
35
39
|
// a stale last-used cookie would immediately shadow what the user just
|
|
36
|
-
// picked in Settings
|
|
40
|
+
// picked in Settings. recordLastUsed: false is for
|
|
37
41
|
// NON-choices: the App self-heal repairs a conflicted persisted pair
|
|
38
42
|
// without any user action, so it must not overwrite the user's memo.
|
|
39
43
|
if (options?.recordLastUsed !== false) {
|
|
@@ -47,8 +51,9 @@ export function setReviewPanelView(
|
|
|
47
51
|
/**
|
|
48
52
|
* The panel view the reviewer has actually persisted, or `undefined` when they
|
|
49
53
|
* never chose one. Distinct from `configStore.get('reviewPanelView')`, which
|
|
50
|
-
* cannot tell a stored choice apart from the built-in default
|
|
51
|
-
*
|
|
54
|
+
* cannot tell a stored choice apart from the built-in default. Plannotator
|
|
55
|
+
* itself no longer needs the distinction (nothing seeds this setting any
|
|
56
|
+
* more); it stays exported for hosts that must not write over a real choice.
|
|
52
57
|
*/
|
|
53
58
|
export function getPersistedReviewPanelView(): 'sections' | 'tree' | undefined {
|
|
54
59
|
return SETTINGS.reviewPanelView.fromCookie();
|