@plannotator/ui 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/components/AISettingsTab.tsx +5 -4
- package/components/ActionMenu.tsx +5 -1
- package/components/AgentsTab.tsx +10 -23
- package/components/AnnotationPanel.tsx +9 -5
- package/components/AnnotationToolbar.tsx +5 -13
- package/components/AnnotationToolstrip.tsx +2 -2
- package/components/ApproveDropdown.tsx +1 -1
- package/components/BlockRenderer.tsx +1 -1
- package/components/CodeFilePopout.tsx +5 -4
- package/components/CommentPopover.tsx +391 -65
- package/components/DocBadges.tsx +29 -11
- package/components/ExportModal.tsx +16 -8
- package/components/GraphvizBlock.tsx +1 -1
- package/components/InlineMarkdown.tsx +30 -13
- package/components/KeyboardShortcuts.tsx +37 -2
- package/components/Landing.tsx +7 -7
- package/components/MenuVersionSection.tsx +4 -4
- package/components/MermaidBlock.tsx +1 -1
- package/components/ModeToggle.tsx +7 -6
- package/components/OpenInAppButton.tsx +2 -5
- package/components/PinpointOverlay.tsx +9 -7
- package/components/PlanHeaderMenu.tsx +8 -8
- package/components/PopoutDialog.tsx +6 -1
- package/components/ResizeHandle.tsx +1 -0
- package/components/Settings.tsx +172 -12
- package/components/SkillReferenceMenu.tsx +260 -0
- package/components/StickyHeaderLane.tsx +7 -0
- package/components/ThemeProvider.tsx +131 -32
- package/components/ThemeTab.tsx +123 -77
- package/components/ToolbarButtons.tsx +29 -8
- package/components/Viewer.tsx +396 -130
- package/components/VimKeyHud.tsx +695 -0
- package/components/VimModeAnnouncementDialog.tsx +557 -0
- package/components/VimModeOverlay.tsx +235 -0
- package/components/VimTargetReticle.tsx +284 -0
- package/components/ai/DocumentAIChatPanel.tsx +1 -1
- package/components/blocks/CodeBlock.tsx +18 -18
- package/components/blocks/TablePopout.tsx +7 -8
- package/components/blocks/TableToolbar.tsx +7 -8
- package/components/goal-setup/GoalSetupSurface.tsx +16 -3
- package/components/html-viewer/HtmlViewer.tsx +450 -47
- package/components/html-viewer/annotationNumbering.ts +37 -0
- package/components/html-viewer/bridge-script.ts +4051 -298
- package/components/html-viewer/composerYield.ts +51 -0
- package/components/html-viewer/srcdoc.ts +18 -3
- package/components/html-viewer/useHtmlAnnotation.ts +457 -32
- package/components/icons/themeIcons.tsx +1 -1
- package/components/plan-diff/PlanCleanDiffView.tsx +9 -9
- package/components/plan-diff/PlanDiffBadge.tsx +22 -1
- package/components/settings/HooksTab.tsx +12 -8
- package/components/sidebar/FileBrowser.tsx +4 -1
- package/components/themeModes.tsx +28 -0
- package/config/configStore.ts +76 -1
- package/config/settings.ts +152 -0
- package/configure.ts +9 -0
- package/globals.d.ts +7 -1
- package/hooks/useAIChat.ts +5 -2
- package/hooks/useAIProviderActivation.ts +47 -0
- package/hooks/useAIProviderConfig.ts +5 -1
- package/hooks/useAgentSettings.ts +64 -23
- package/hooks/useAgents.ts +4 -4
- package/hooks/useAnnotationHighlighter.ts +100 -3
- package/hooks/useArchive.ts +2 -1
- package/hooks/useFenceTheme.ts +17 -0
- package/hooks/useLinkedDoc.ts +68 -1
- package/hooks/usePinpoint.ts +76 -75
- package/hooks/usePlanDiff.ts +73 -2
- package/hooks/useSkillReferenceAutocomplete.ts +239 -0
- package/hooks/useUpdateCheck.ts +1 -2
- package/hooks/useVimDocumentFocus.ts +116 -0
- package/hooks/useVimSelection.ts +1063 -0
- package/package.json +4 -4
- package/print.css +14 -13
- package/shortcuts/core.ts +38 -13
- package/shortcuts/index.ts +10 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +7 -0
- package/shortcuts/plan-review/vimSelection.shortcuts.ts +251 -0
- package/shortcuts/runtime.ts +111 -12
- package/styles.css +1 -1
- package/theme.css +504 -0
- package/themes/colorblind.css +89 -0
- package/themes/plannotator.css +2 -2
- package/types.ts +93 -10
- package/utils/agentSwitch.ts +33 -7
- package/utils/blockTargeting.ts +462 -178
- package/utils/clipboard.ts +110 -0
- package/utils/codeBlockMark.ts +50 -0
- package/utils/codeHighlight.ts +293 -0
- package/utils/codexModels.ts +79 -0
- package/utils/domSelection.ts +84 -0
- package/utils/htmlChrome.ts +73 -0
- package/utils/inputMethod.ts +79 -6
- package/utils/parser.ts +517 -21
- package/utils/preferenceTtl.ts +15 -0
- package/utils/sharing.ts +0 -1
- package/utils/skillCatalog.ts +269 -0
- package/utils/skillReferences.ts +475 -0
- package/utils/syntaxTheme.ts +83 -0
- package/utils/themeRegistry.ts +154 -0
- package/utils/vimHud.ts +263 -0
- package/utils/vimModeAnnouncement.ts +23 -0
- package/utils/vimNavigation.ts +417 -0
- package/utils/vimReticle.ts +88 -0
- package/utils/vimScroll.ts +162 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import { getItem, setItem } from '../utils/storage';
|
|
3
|
+
import { clampCodexReasoning } from '../utils/codexModels';
|
|
3
4
|
|
|
4
5
|
const COOKIE_KEY = 'plannotator.agents';
|
|
5
6
|
|
|
@@ -15,7 +16,7 @@ const COOKIE_KEY = 'plannotator.agents';
|
|
|
15
16
|
// would clobber writes exactly like before.
|
|
16
17
|
const settingsListeners = new Set<(s: AgentSettingsState) => void>();
|
|
17
18
|
|
|
18
|
-
export const DEFAULT_CLAUDE_MODEL = 'claude-opus-
|
|
19
|
+
export const DEFAULT_CLAUDE_MODEL = 'claude-opus-5';
|
|
19
20
|
export const DEFAULT_CLAUDE_EFFORT = 'high';
|
|
20
21
|
// gpt-5.3-codex is deprecated (ChatGPT-account Codex rejects it outright) —
|
|
21
22
|
// default to the current flagship everywhere.
|
|
@@ -176,7 +177,9 @@ const initialState: AgentSettingsState = {
|
|
|
176
177
|
|
|
177
178
|
// One-shot migration: drop any cached "none" codex reasoning entries. The
|
|
178
179
|
// dropdown no longer offers "None" (codex-rs rejects it as a config value);
|
|
179
|
-
// fall back to the default instead of shipping an invalid flag.
|
|
180
|
+
// fall back to the default instead of shipping an invalid flag. Saved
|
|
181
|
+
// "minimal" entries migrate to "low": no current Codex model supports
|
|
182
|
+
// minimal, and low is the nearest effort that every model does.
|
|
180
183
|
export function sanitizeCodexPerModel(
|
|
181
184
|
perModel: Record<string, { reasoning: string; fast: boolean }> | undefined,
|
|
182
185
|
): Record<string, { reasoning: string; fast: boolean }> {
|
|
@@ -188,17 +191,50 @@ export function sanitizeCodexPerModel(
|
|
|
188
191
|
if (entry.fast) out[model] = { reasoning: DEFAULT_CODEX_REASONING, fast: true };
|
|
189
192
|
continue;
|
|
190
193
|
}
|
|
194
|
+
if (entry.reasoning === 'minimal') {
|
|
195
|
+
out[model] = { ...entry, reasoning: 'low' };
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
191
198
|
out[model] = entry;
|
|
192
199
|
}
|
|
193
200
|
return out;
|
|
194
201
|
}
|
|
195
202
|
|
|
196
|
-
//
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
// Saved model IDs that migrate to a direct replacement: the stale gpt-5.6
|
|
204
|
+
// slug (renamed to -sol when the tiered family shipped) and gpt-5.1-codex-mini
|
|
205
|
+
// (API shutdown 2026-07-23; gpt-5.4-mini is OpenAI's recommended replacement).
|
|
206
|
+
const RENAMED_CODEX_MODELS: Record<string, string> = {
|
|
207
|
+
'gpt-5.6': 'gpt-5.6-sol',
|
|
208
|
+
'gpt-5.1-codex-mini': 'gpt-5.4-mini',
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Saved model IDs with no direct replacement — migrate to the surface's
|
|
212
|
+
// fallback. gpt-5.3-codex is rejected outright by ChatGPT-account Codex;
|
|
213
|
+
// gpt-5.2-codex and gpt-5.1-codex-max hit the API-level shutdown on
|
|
214
|
+
// 2026-07-23 (OpenAI recommends gpt-5.5, which every fallback already is).
|
|
215
|
+
const RETIRED_CODEX_MODELS = new Set(['gpt-5.3-codex', 'gpt-5.2-codex', 'gpt-5.1-codex-max']);
|
|
216
|
+
|
|
217
|
+
// One-shot model migrations for a saved Codex section. Keep its per-model
|
|
218
|
+
// preferences aligned with the canonical model ID while sanitizing them.
|
|
219
|
+
export function migrateCodexSection(
|
|
220
|
+
section: { model?: unknown; perModel?: Record<string, { reasoning: string; fast: boolean }> } | undefined,
|
|
221
|
+
fallback: string,
|
|
222
|
+
): CodexSection {
|
|
223
|
+
const perModel = sanitizeCodexPerModel(section?.perModel);
|
|
224
|
+
for (const [legacy, replacement] of Object.entries(RENAMED_CODEX_MODELS)) {
|
|
225
|
+
const legacyPreference = perModel[legacy];
|
|
226
|
+
if (legacyPreference) {
|
|
227
|
+
perModel[replacement] ??= legacyPreference;
|
|
228
|
+
delete perModel[legacy];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const savedModel = section?.model;
|
|
233
|
+
const model =
|
|
234
|
+
typeof savedModel !== 'string' || RETIRED_CODEX_MODELS.has(savedModel)
|
|
235
|
+
? fallback
|
|
236
|
+
: (RENAMED_CODEX_MODELS[savedModel] ?? savedModel);
|
|
237
|
+
return { model, perModel };
|
|
202
238
|
}
|
|
203
239
|
|
|
204
240
|
function parseEngine(value: unknown): AgentEngine {
|
|
@@ -248,10 +284,7 @@ function readCookie(): AgentSettingsState {
|
|
|
248
284
|
model: typeof parsed.claude?.model === 'string' ? parsed.claude.model : DEFAULT_CLAUDE_MODEL,
|
|
249
285
|
perModel: parsed.claude?.perModel ?? {},
|
|
250
286
|
},
|
|
251
|
-
codex:
|
|
252
|
-
model: migrateCodexModel(parsed.codex?.model, DEFAULT_CODEX_MODEL),
|
|
253
|
-
perModel: sanitizeCodexPerModel(parsed.codex?.perModel),
|
|
254
|
-
},
|
|
287
|
+
codex: migrateCodexSection(parsed.codex, DEFAULT_CODEX_MODEL),
|
|
255
288
|
cursor: {
|
|
256
289
|
model: typeof parsed.cursor?.model === 'string' ? parsed.cursor.model : DEFAULT_CURSOR_MODEL,
|
|
257
290
|
},
|
|
@@ -269,18 +302,12 @@ function readCookie(): AgentSettingsState {
|
|
|
269
302
|
model: typeof parsed.tourClaude?.model === 'string' ? parsed.tourClaude.model : DEFAULT_TOUR_CLAUDE_MODEL,
|
|
270
303
|
perModel: parsed.tourClaude?.perModel ?? {},
|
|
271
304
|
},
|
|
272
|
-
tourCodex:
|
|
273
|
-
model: migrateCodexModel(parsed.tourCodex?.model, DEFAULT_TOUR_CODEX_MODEL),
|
|
274
|
-
perModel: sanitizeCodexPerModel(parsed.tourCodex?.perModel),
|
|
275
|
-
},
|
|
305
|
+
tourCodex: migrateCodexSection(parsed.tourCodex, DEFAULT_TOUR_CODEX_MODEL),
|
|
276
306
|
guideClaude: {
|
|
277
307
|
model: typeof parsed.guideClaude?.model === 'string' ? parsed.guideClaude.model : DEFAULT_GUIDE_CLAUDE_MODEL,
|
|
278
308
|
perModel: parsed.guideClaude?.perModel ?? {},
|
|
279
309
|
},
|
|
280
|
-
guideCodex:
|
|
281
|
-
model: migrateCodexModel(parsed.guideCodex?.model, DEFAULT_GUIDE_CODEX_MODEL),
|
|
282
|
-
perModel: sanitizeCodexPerModel(parsed.guideCodex?.perModel),
|
|
283
|
-
},
|
|
310
|
+
guideCodex: migrateCodexSection(parsed.guideCodex, DEFAULT_GUIDE_CODEX_MODEL),
|
|
284
311
|
guideCursor: {
|
|
285
312
|
model: typeof parsed.guideCursor?.model === 'string' ? parsed.guideCursor.model : DEFAULT_GUIDE_CURSOR_MODEL,
|
|
286
313
|
},
|
|
@@ -508,13 +535,27 @@ export function useAgentSettings() {
|
|
|
508
535
|
}, []);
|
|
509
536
|
|
|
510
537
|
const claudeEffort = state.claude.perModel[state.claude.model]?.effort ?? DEFAULT_CLAUDE_EFFORT;
|
|
511
|
-
|
|
538
|
+
// Codex reasoning is clamped through the model's supported-effort set: a
|
|
539
|
+
// saved (or surface-default) effort the selected model doesn't support
|
|
540
|
+
// snaps to that model's catalog default. Every consumer — the pickers AND
|
|
541
|
+
// the launch payloads — reads these derived values, so an unsupported
|
|
542
|
+
// effort can never reach `-c model_reasoning_effort=`.
|
|
543
|
+
const codexReasoning = clampCodexReasoning(
|
|
544
|
+
state.codex.model,
|
|
545
|
+
state.codex.perModel[state.codex.model]?.reasoning ?? DEFAULT_CODEX_REASONING,
|
|
546
|
+
);
|
|
512
547
|
const codexFast = state.codex.perModel[state.codex.model]?.fast ?? DEFAULT_CODEX_FAST;
|
|
513
548
|
const tourClaudeEffort = state.tourClaude.perModel[state.tourClaude.model]?.effort ?? DEFAULT_TOUR_CLAUDE_EFFORT;
|
|
514
|
-
const tourCodexReasoning =
|
|
549
|
+
const tourCodexReasoning = clampCodexReasoning(
|
|
550
|
+
state.tourCodex.model,
|
|
551
|
+
state.tourCodex.perModel[state.tourCodex.model]?.reasoning ?? DEFAULT_TOUR_CODEX_REASONING,
|
|
552
|
+
);
|
|
515
553
|
const tourCodexFast = state.tourCodex.perModel[state.tourCodex.model]?.fast ?? DEFAULT_TOUR_CODEX_FAST;
|
|
516
554
|
const guideClaudeEffort = state.guideClaude.perModel[state.guideClaude.model]?.effort ?? DEFAULT_GUIDE_CLAUDE_EFFORT;
|
|
517
|
-
const guideCodexReasoning =
|
|
555
|
+
const guideCodexReasoning = clampCodexReasoning(
|
|
556
|
+
state.guideCodex.model,
|
|
557
|
+
state.guideCodex.perModel[state.guideCodex.model]?.reasoning ?? DEFAULT_GUIDE_CODEX_REASONING,
|
|
558
|
+
);
|
|
518
559
|
|
|
519
560
|
return {
|
|
520
561
|
selectedMode: state.selectedMode,
|
package/hooks/useAgents.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { useState, useEffect, useCallback } from 'react';
|
|
6
6
|
import type { Origin } from '@plannotator/core/agents';
|
|
7
|
-
import { getAgentSwitchSettings } from '../utils/agentSwitch';
|
|
7
|
+
import { getAgentSwitchSettings, type AgentSwitchSurface } from '../utils/agentSwitch';
|
|
8
8
|
|
|
9
9
|
export interface Agent {
|
|
10
10
|
id: string;
|
|
@@ -25,7 +25,7 @@ export interface UseAgentsResult {
|
|
|
25
25
|
* Fetch available agents from OpenCode API
|
|
26
26
|
* Only fetches when origin is 'opencode'
|
|
27
27
|
*/
|
|
28
|
-
export function useAgents(origin: Origin | null): UseAgentsResult {
|
|
28
|
+
export function useAgents(origin: Origin | null, surface: AgentSwitchSurface = 'plan'): UseAgentsResult {
|
|
29
29
|
const [agents, setAgents] = useState<Agent[]>([]);
|
|
30
30
|
const [isLoading, setIsLoading] = useState(false);
|
|
31
31
|
|
|
@@ -56,7 +56,7 @@ export function useAgents(origin: Origin | null): UseAgentsResult {
|
|
|
56
56
|
const getAgentWarning = useCallback((): string | null => {
|
|
57
57
|
if (agents.length === 0) return null; // Can't validate without agents
|
|
58
58
|
|
|
59
|
-
const settings = getAgentSwitchSettings();
|
|
59
|
+
const settings = getAgentSwitchSettings(surface);
|
|
60
60
|
|
|
61
61
|
if (settings.switchTo === 'disabled') {
|
|
62
62
|
return null;
|
|
@@ -79,7 +79,7 @@ export function useAgents(origin: Origin | null): UseAgentsResult {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
return null;
|
|
82
|
-
}, [agents, validateAgent]);
|
|
82
|
+
}, [agents, validateAgent, surface]);
|
|
83
83
|
|
|
84
84
|
return {
|
|
85
85
|
agents,
|
|
@@ -244,6 +244,7 @@ export interface UseAnnotationHighlighterOptions {
|
|
|
244
244
|
onRestoreMismatch?: (annotation: Annotation, restoredText: string) => void;
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/** Annotation UI state and mutation commands owned by one rendered document. */
|
|
247
248
|
export interface UseAnnotationHighlighterReturn {
|
|
248
249
|
highlighterRef: RefObject<Highlighter | null>;
|
|
249
250
|
|
|
@@ -259,12 +260,22 @@ export interface UseAnnotationHighlighterReturn {
|
|
|
259
260
|
handleCommentClose: () => void;
|
|
260
261
|
handleFloatingQuickLabel: (label: QuickLabel) => void;
|
|
261
262
|
handleQuickLabelPickerDismiss: () => void;
|
|
263
|
+
/** Paint a caller-created DOM range through the same pipeline as pointer selection. */
|
|
264
|
+
highlightRange: (range: Range, modeOverride?: EditorMode) => void;
|
|
265
|
+
/** Annotate one rendered formula through the same path as a pointer click. */
|
|
266
|
+
highlightMathElement: (element: HTMLElement, modeOverride?: EditorMode) => void;
|
|
262
267
|
|
|
263
268
|
removeHighlight: (id: string) => void;
|
|
264
269
|
clearAllHighlights: () => void;
|
|
265
270
|
applyAnnotations: (annotations: Annotation[]) => void;
|
|
266
271
|
}
|
|
267
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Own pointer and caller-supplied range annotations for a Markdown container.
|
|
275
|
+
*
|
|
276
|
+
* Highlight restoration failures are reported through `onRestoreMismatch`
|
|
277
|
+
* when verification is enabled; malformed or stale ranges are ignored.
|
|
278
|
+
*/
|
|
268
279
|
export function useAnnotationHighlighter({
|
|
269
280
|
containerRef,
|
|
270
281
|
annotations,
|
|
@@ -283,6 +294,7 @@ export function useAnnotationHighlighter({
|
|
|
283
294
|
const pendingSourceRef = useRef<any>(null);
|
|
284
295
|
const pendingMathTargetsRef = useRef<MathAnnotationTarget[]>([]);
|
|
285
296
|
const pendingMathElementRef = useRef<HTMLElement | null>(null);
|
|
297
|
+
const pendingModeOverrideRef = useRef<EditorMode | null>(null);
|
|
286
298
|
const justCreatedIdRef = useRef<string | null>(null);
|
|
287
299
|
const lastMousePosRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 });
|
|
288
300
|
const mouseDownMathRef = useRef<HTMLElement | null>(null);
|
|
@@ -834,10 +846,13 @@ export function useAnnotationHighlighter({
|
|
|
834
846
|
setCommentPopover(null);
|
|
835
847
|
setQuickLabelPicker(null);
|
|
836
848
|
|
|
837
|
-
|
|
849
|
+
const effectiveMode = pendingModeOverrideRef.current ?? modeRef.current;
|
|
850
|
+
pendingModeOverrideRef.current = null;
|
|
851
|
+
|
|
852
|
+
if (effectiveMode === 'redline') {
|
|
838
853
|
createAnnotationFromSource(highlighter, source, AnnotationType.DELETION);
|
|
839
854
|
window.getSelection()?.removeAllRanges();
|
|
840
|
-
} else if (
|
|
855
|
+
} else if (effectiveMode === 'comment') {
|
|
841
856
|
pendingSourceRef.current = source;
|
|
842
857
|
setCommentPopover({
|
|
843
858
|
anchorEl: doms[0] as HTMLElement,
|
|
@@ -845,7 +860,7 @@ export function useAnnotationHighlighter({
|
|
|
845
860
|
selectedText: source.text,
|
|
846
861
|
source,
|
|
847
862
|
});
|
|
848
|
-
} else if (
|
|
863
|
+
} else if (effectiveMode === 'quickLabel') {
|
|
849
864
|
pendingSourceRef.current = source;
|
|
850
865
|
setQuickLabelPicker({
|
|
851
866
|
anchorEl: doms[0] as HTMLElement,
|
|
@@ -986,6 +1001,86 @@ export function useAnnotationHighlighter({
|
|
|
986
1001
|
};
|
|
987
1002
|
}, [clearPendingSelection, enabled]);
|
|
988
1003
|
|
|
1004
|
+
const highlightRange = useCallback((range: Range, modeOverride?: EditorMode) => {
|
|
1005
|
+
const highlighter = highlighterRef.current;
|
|
1006
|
+
const container = containerRef.current;
|
|
1007
|
+
if (!highlighter || !container || range.collapsed) return;
|
|
1008
|
+
if (!container.contains(range.commonAncestorContainer)) return;
|
|
1009
|
+
|
|
1010
|
+
const selection = window.getSelection();
|
|
1011
|
+
selection?.removeAllRanges();
|
|
1012
|
+
selection?.addRange(range.cloneRange());
|
|
1013
|
+
pendingModeOverrideRef.current = modeOverride ?? null;
|
|
1014
|
+
|
|
1015
|
+
try {
|
|
1016
|
+
highlighter.fromRange(range);
|
|
1017
|
+
} finally {
|
|
1018
|
+
pendingModeOverrideRef.current = null;
|
|
1019
|
+
selection?.removeAllRanges();
|
|
1020
|
+
}
|
|
1021
|
+
}, [containerRef]);
|
|
1022
|
+
|
|
1023
|
+
const highlightMathElement = useCallback((
|
|
1024
|
+
element: HTMLElement,
|
|
1025
|
+
modeOverride?: EditorMode,
|
|
1026
|
+
) => {
|
|
1027
|
+
const container = containerRef.current;
|
|
1028
|
+
const mathElement = closestMathElement(element, container);
|
|
1029
|
+
if (!container || !mathElement) return;
|
|
1030
|
+
|
|
1031
|
+
const effectiveMode = modeOverride ?? modeRef.current;
|
|
1032
|
+
const existingId = mathElement.dataset.bindId;
|
|
1033
|
+
if (existingId && effectiveMode !== 'redline') {
|
|
1034
|
+
onSelectAnnotationRef.current?.(existingId);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
const source = mathSourceFromElement(mathElement);
|
|
1039
|
+
if (!source) return;
|
|
1040
|
+
|
|
1041
|
+
const highlighter = highlighterRef.current;
|
|
1042
|
+
if (pendingSourceRef.current && highlighter && !isMathAnnotationSource(pendingSourceRef.current)) {
|
|
1043
|
+
highlighter.remove(pendingSourceRef.current.id);
|
|
1044
|
+
}
|
|
1045
|
+
clearPendingSelection();
|
|
1046
|
+
setToolbarState(null);
|
|
1047
|
+
setCommentPopover(null);
|
|
1048
|
+
setQuickLabelPicker(null);
|
|
1049
|
+
|
|
1050
|
+
if (effectiveMode === 'redline') {
|
|
1051
|
+
createAnnotationFromMathSource(source, AnnotationType.DELETION);
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
pendingSourceRef.current = source;
|
|
1056
|
+
showPendingMathPreview(source);
|
|
1057
|
+
|
|
1058
|
+
if (effectiveMode === 'comment') {
|
|
1059
|
+
setCommentPopover({
|
|
1060
|
+
anchorEl: source.element,
|
|
1061
|
+
contextText: source.text.slice(0, 80),
|
|
1062
|
+
selectedText: source.text,
|
|
1063
|
+
source,
|
|
1064
|
+
});
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (effectiveMode === 'quickLabel') {
|
|
1069
|
+
setQuickLabelPicker({
|
|
1070
|
+
anchorEl: source.element,
|
|
1071
|
+
cursorHint: lastMousePosRef.current,
|
|
1072
|
+
source,
|
|
1073
|
+
});
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
setToolbarState({
|
|
1078
|
+
element: source.element,
|
|
1079
|
+
source,
|
|
1080
|
+
selectionText: source.text,
|
|
1081
|
+
});
|
|
1082
|
+
}, [clearPendingSelection, containerRef, showPendingMathPreview]);
|
|
1083
|
+
|
|
989
1084
|
// Apply CSS classes to existing annotations
|
|
990
1085
|
useEffect(() => {
|
|
991
1086
|
const highlighter = highlighterRef.current;
|
|
@@ -1201,6 +1296,8 @@ export function useAnnotationHighlighter({
|
|
|
1201
1296
|
handleCommentClose,
|
|
1202
1297
|
handleFloatingQuickLabel,
|
|
1203
1298
|
handleQuickLabelPickerDismiss,
|
|
1299
|
+
highlightRange,
|
|
1300
|
+
highlightMathElement,
|
|
1204
1301
|
removeHighlight,
|
|
1205
1302
|
clearAllHighlights,
|
|
1206
1303
|
applyAnnotations: applyAnnotationsInternal,
|
package/hooks/useArchive.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { ArchivedPlan } from "@plannotator/core/storage-types";
|
|
|
10
10
|
import type { UseLinkedDocReturn } from "./useLinkedDoc";
|
|
11
11
|
import type { ViewerHandle } from "../components/Viewer";
|
|
12
12
|
import type { Annotation } from "../types";
|
|
13
|
+
import { copyTextToClipboard } from "../utils/clipboard";
|
|
13
14
|
import { getPlanSaveSettings } from "../utils/planSave";
|
|
14
15
|
|
|
15
16
|
export interface UseArchiveOptions {
|
|
@@ -148,7 +149,7 @@ export function useArchive(options: UseArchiveOptions): UseArchiveReturn {
|
|
|
148
149
|
}, [setSubmitted]);
|
|
149
150
|
|
|
150
151
|
const copy = useCallback(() => {
|
|
151
|
-
|
|
152
|
+
void copyTextToClipboard(markdown);
|
|
152
153
|
}, [markdown]);
|
|
153
154
|
|
|
154
155
|
const clearSelection = useCallback(() => {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useTheme } from '../components/ThemeProvider';
|
|
2
|
+
import { resolveFenceTheme } from '../utils/syntaxTheme';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The Shiki theme name that code snippets should render in right now.
|
|
6
|
+
*
|
|
7
|
+
* Same (colorTheme, mode) resolution the code-review diff pane uses, so fences,
|
|
8
|
+
* suggestion cards and diff hunks all agree. Re-renders on palette or mode
|
|
9
|
+
* change, which is what drives the re-highlight in the components below.
|
|
10
|
+
*
|
|
11
|
+
* `ThemeProvider`'s default context supplies the Plannotator palette in dark
|
|
12
|
+
* mode, so this is safe to call outside a provider.
|
|
13
|
+
*/
|
|
14
|
+
export function useFenceTheme(): string {
|
|
15
|
+
const { colorTheme, resolvedMode } = useTheme();
|
|
16
|
+
return resolveFenceTheme(colorTheme, resolvedMode ?? 'dark');
|
|
17
|
+
}
|
package/hooks/useLinkedDoc.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type { Annotation, ImageAttachment } from "../types";
|
|
|
11
11
|
import type { ViewerHandle } from "../components/Viewer";
|
|
12
12
|
import type { SidebarTab } from "./useSidebar";
|
|
13
13
|
import type { SourceSaveCapability } from "@plannotator/core/source-save";
|
|
14
|
+
import type { VersionInfo } from "./usePlanDiff";
|
|
14
15
|
|
|
15
16
|
export interface LinkedDocLoadData {
|
|
16
17
|
markdown?: string;
|
|
@@ -20,6 +21,43 @@ export interface LinkedDocLoadData {
|
|
|
20
21
|
rawHtml?: string;
|
|
21
22
|
shareHtml?: string;
|
|
22
23
|
sourceSave?: SourceSaveCapability;
|
|
24
|
+
/**
|
|
25
|
+
* Per-file version-diff baseline (annotate folder sessions only) — the same
|
|
26
|
+
* field names/shapes /api/plan already returns for single-file sessions.
|
|
27
|
+
* /api/doc only populates these for eligible folder files (local annotatable
|
|
28
|
+
* plain-text files — .md/.mdx/.txt plus the config set, per
|
|
29
|
+
* `isAnnotatableTextPath` — markdown-branch, not converted/HTML); every other
|
|
30
|
+
* document — including
|
|
31
|
+
* plain linked docs outside folder mode — omits them, which is what keeps
|
|
32
|
+
* the diff badge/version browser from appearing for those.
|
|
33
|
+
*/
|
|
34
|
+
previousPlan?: string | null;
|
|
35
|
+
versionInfo?: VersionInfo | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the version-diff baseline for a document being activated: prefer
|
|
40
|
+
* an already-cached baseline (set the first time this document was opened)
|
|
41
|
+
* over the freshly-fetched `data`, so re-opening a document during a session
|
|
42
|
+
* never loses — or needs to re-request — its baseline. Pure so it's testable
|
|
43
|
+
* without mounting the hook; used by activateDocument below.
|
|
44
|
+
*
|
|
45
|
+
* Gated on whether a baseline was EVER captured for this document (versionInfo
|
|
46
|
+
* presence), not on truthiness of the cached previousPlan value — a document
|
|
47
|
+
* at its first-ever version legitimately caches `previousPlan: null` ("no
|
|
48
|
+
* earlier version to diff against yet"), which is a resolved fact, not a
|
|
49
|
+
* cache miss. Falling through to `data` on a plain `??` would silently
|
|
50
|
+
* discard that fact whenever `data` lacks the fields too (e.g. the
|
|
51
|
+
* missing-on-disk openLoaded path, which never fetches /api/doc).
|
|
52
|
+
*/
|
|
53
|
+
export function resolveDiffBaseline(
|
|
54
|
+
cached: Pick<CachedDocState, 'previousPlan' | 'versionInfo'> | undefined,
|
|
55
|
+
data: Pick<LinkedDocLoadData, 'previousPlan' | 'versionInfo'>,
|
|
56
|
+
): { previousPlan: string | null | undefined; versionInfo: VersionInfo | null | undefined } {
|
|
57
|
+
if (cached && cached.versionInfo !== undefined) {
|
|
58
|
+
return { previousPlan: cached.previousPlan, versionInfo: cached.versionInfo };
|
|
59
|
+
}
|
|
60
|
+
return { previousPlan: data.previousPlan, versionInfo: data.versionInfo };
|
|
23
61
|
}
|
|
24
62
|
|
|
25
63
|
export interface UseLinkedDocOptions {
|
|
@@ -73,6 +111,12 @@ export interface CachedDocState {
|
|
|
73
111
|
globalAttachments: ImageAttachment[];
|
|
74
112
|
markdown?: string;
|
|
75
113
|
isConverted?: boolean;
|
|
114
|
+
/** Version-diff baseline captured the first time this document was
|
|
115
|
+
* activated — see LinkedDocLoadData. Persisted here so re-opening the
|
|
116
|
+
* same document later in the session reuses it instead of losing it (or
|
|
117
|
+
* needing to re-fetch it) once the fresh /api/doc data is gone. */
|
|
118
|
+
previousPlan?: string | null;
|
|
119
|
+
versionInfo?: VersionInfo | null;
|
|
76
120
|
}
|
|
77
121
|
|
|
78
122
|
export interface LinkedDocSessionState {
|
|
@@ -109,6 +153,12 @@ export interface UseLinkedDocReturn {
|
|
|
109
153
|
restoreSession: (state: LinkedDocSessionState) => void;
|
|
110
154
|
/** Reactive count of annotations on non-active documents (updates on open() and back()) */
|
|
111
155
|
docAnnotationCount: number;
|
|
156
|
+
/** Active document's version-diff baseline (folder annotate only). Null
|
|
157
|
+
* when no document is active, or the active document has no eligible
|
|
158
|
+
* history — including every non-folder linked doc, since /api/doc never
|
|
159
|
+
* populates these fields for those. */
|
|
160
|
+
diffPreviousPlan: string | null;
|
|
161
|
+
diffVersionInfo: VersionInfo | null;
|
|
112
162
|
}
|
|
113
163
|
|
|
114
164
|
const HIGHLIGHT_REAPPLY_DELAY = 100;
|
|
@@ -139,7 +189,13 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
139
189
|
onAfterBack,
|
|
140
190
|
} = options;
|
|
141
191
|
|
|
142
|
-
const [linkedDoc, setLinkedDoc] = useState<{
|
|
192
|
+
const [linkedDoc, setLinkedDoc] = useState<{
|
|
193
|
+
filepath: string;
|
|
194
|
+
isConverted?: boolean;
|
|
195
|
+
markdown?: string;
|
|
196
|
+
previousPlan?: string | null;
|
|
197
|
+
versionInfo?: VersionInfo | null;
|
|
198
|
+
} | null>(null);
|
|
143
199
|
const [error, setError] = useState<string | null>(null);
|
|
144
200
|
const [isLoading, setIsLoading] = useState(false);
|
|
145
201
|
const [docAnnotationCount, setDocAnnotationCount] = useState(0);
|
|
@@ -169,6 +225,8 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
169
225
|
globalAttachments: [...globalAttachments],
|
|
170
226
|
markdown: getDocumentMarkdown?.(linkedDoc.filepath, linkedDoc.markdown) ?? linkedDoc.markdown,
|
|
171
227
|
isConverted: linkedDoc.isConverted,
|
|
228
|
+
previousPlan: linkedDoc.previousPlan,
|
|
229
|
+
versionInfo: linkedDoc.versionInfo,
|
|
172
230
|
});
|
|
173
231
|
// Update reactive count so button labels can respond
|
|
174
232
|
let total = 0;
|
|
@@ -264,6 +322,8 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
264
322
|
globalAttachments: [...globalAttachments],
|
|
265
323
|
markdown: getDocumentMarkdown?.(linkedDoc.filepath, linkedDoc.markdown) ?? linkedDoc.markdown,
|
|
266
324
|
isConverted: linkedDoc.isConverted,
|
|
325
|
+
previousPlan: linkedDoc.previousPlan,
|
|
326
|
+
versionInfo: linkedDoc.versionInfo,
|
|
267
327
|
});
|
|
268
328
|
let total = 0;
|
|
269
329
|
for (const [fp, cached] of docCache.current.entries()) {
|
|
@@ -294,10 +354,13 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
294
354
|
setAnnotations(cached?.annotations ?? []);
|
|
295
355
|
setGlobalAttachments(cached?.globalAttachments ?? []);
|
|
296
356
|
setSelectedAnnotationId(null);
|
|
357
|
+
const diffBaseline = resolveDiffBaseline(cached, data);
|
|
297
358
|
setLinkedDoc({
|
|
298
359
|
filepath: data.filepath,
|
|
299
360
|
isConverted: !!data.isConverted,
|
|
300
361
|
markdown: nextMarkdown,
|
|
362
|
+
previousPlan: diffBaseline.previousPlan,
|
|
363
|
+
versionInfo: diffBaseline.versionInfo,
|
|
301
364
|
});
|
|
302
365
|
setError(null);
|
|
303
366
|
sidebar.open(targetTab ?? "toc");
|
|
@@ -391,6 +454,8 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
391
454
|
globalAttachments: [...globalAttachments],
|
|
392
455
|
markdown: getDocumentMarkdown?.(linkedDoc.filepath, linkedDoc.markdown) ?? linkedDoc.markdown,
|
|
393
456
|
isConverted: linkedDoc.isConverted,
|
|
457
|
+
previousPlan: linkedDoc.previousPlan,
|
|
458
|
+
versionInfo: linkedDoc.versionInfo,
|
|
394
459
|
});
|
|
395
460
|
}
|
|
396
461
|
|
|
@@ -490,5 +555,7 @@ export function useLinkedDoc(options: UseLinkedDocOptions): UseLinkedDocReturn {
|
|
|
490
555
|
snapshotSession,
|
|
491
556
|
restoreSession,
|
|
492
557
|
docAnnotationCount,
|
|
558
|
+
diffPreviousPlan: linkedDoc?.previousPlan ?? null,
|
|
559
|
+
diffVersionInfo: linkedDoc?.versionInfo ?? null,
|
|
493
560
|
};
|
|
494
561
|
}
|