@plannotator/ui 0.22.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 +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- package/utils/wideMode.ts +48 -0
package/types.ts
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
export enum AnnotationType {
|
|
2
|
+
DELETION = 'DELETION',
|
|
3
|
+
COMMENT = 'COMMENT',
|
|
4
|
+
GLOBAL_COMMENT = 'GLOBAL_COMMENT',
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type EditorMode = 'selection' | 'comment' | 'redline' | 'quickLabel';
|
|
8
|
+
|
|
9
|
+
export type InputMethod = 'drag' | 'pinpoint';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Compactness of the Viewer action button labels (Image / Comment / Copy).
|
|
13
|
+
* Driven by measured plan-area width so the cluster collapses responsively
|
|
14
|
+
* when the side panel squeezes the plan.
|
|
15
|
+
* full → "Global comment" / "Copy plan"
|
|
16
|
+
* short → "Comment" / "Copy"
|
|
17
|
+
* icon → labels hidden entirely
|
|
18
|
+
*/
|
|
19
|
+
export type ActionsLabelMode = 'full' | 'short' | 'icon';
|
|
20
|
+
|
|
21
|
+
export type WideModeType = 'wide' | 'focus';
|
|
22
|
+
|
|
23
|
+
export interface ImageAttachment {
|
|
24
|
+
path: string;
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Annotation {
|
|
29
|
+
id: string;
|
|
30
|
+
blockId: string; // Legacy - not used with web-highlighter
|
|
31
|
+
startOffset: number; // Legacy
|
|
32
|
+
endOffset: number; // Legacy
|
|
33
|
+
type: AnnotationType;
|
|
34
|
+
text?: string; // For comments
|
|
35
|
+
originalText: string; // The text that was selected
|
|
36
|
+
createdA: number;
|
|
37
|
+
author?: string; // Tater identity for collaborative sharing
|
|
38
|
+
source?: string; // External tool identifier (e.g., "eslint") — set when annotation comes from external API
|
|
39
|
+
images?: ImageAttachment[]; // Attached images with human-readable names
|
|
40
|
+
isQuickLabel?: boolean; // true if created via quick label chip
|
|
41
|
+
quickLabelTip?: string; // optional instruction tip from the label definition
|
|
42
|
+
diffContext?: 'added' | 'removed' | 'modified'; // set when annotation created in plan diff view
|
|
43
|
+
mathTargets?: Array<{
|
|
44
|
+
blockId: string;
|
|
45
|
+
tex: string;
|
|
46
|
+
displayMode: boolean;
|
|
47
|
+
}>; // math elements covered by a mixed text+formula selection
|
|
48
|
+
prUrl?: string; // code-review PR mode: the PR this note belongs to, so it isn't shown/exported against another PR after an in-place switch
|
|
49
|
+
// web-highlighter metadata for cross-element selections
|
|
50
|
+
startMeta?: {
|
|
51
|
+
parentTagName: string;
|
|
52
|
+
parentIndex: number;
|
|
53
|
+
textOffset: number;
|
|
54
|
+
};
|
|
55
|
+
endMeta?: {
|
|
56
|
+
parentTagName: string;
|
|
57
|
+
parentIndex: number;
|
|
58
|
+
textOffset: number;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type AlertKind = 'note' | 'tip' | 'warning' | 'caution' | 'important';
|
|
63
|
+
|
|
64
|
+
export interface Block {
|
|
65
|
+
id: string;
|
|
66
|
+
type: 'paragraph' | 'heading' | 'blockquote' | 'list-item' | 'code' | 'hr' | 'table' | 'html' | 'directive' | 'math';
|
|
67
|
+
content: string; // Plain text, or raw (unsanitized) HTML for type === 'html'
|
|
68
|
+
level?: number; // For headings (1-6) or list indentation
|
|
69
|
+
language?: string; // For code blocks (e.g., 'rust', 'typescript')
|
|
70
|
+
checked?: boolean; // For checkbox list items (true = checked, false = unchecked, undefined = not a checkbox)
|
|
71
|
+
ordered?: boolean; // For list items: true when source marker was \d+.
|
|
72
|
+
orderedStart?: number; // For ordered list items: integer parsed from the marker (e.g. 5 for "5.")
|
|
73
|
+
alertKind?: AlertKind; // For blockquotes starting with [!NOTE] / [!TIP] / etc.
|
|
74
|
+
directiveKind?: string; // For directive containers (e.g. ':::note' → 'note')
|
|
75
|
+
order: number; // Sorting order
|
|
76
|
+
startLine: number; // 1-based line number in source
|
|
77
|
+
sourceLineCount?: number; // Number of source lines consumed when it differs from content lines
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DiffResult {
|
|
81
|
+
original: string;
|
|
82
|
+
modified: string;
|
|
83
|
+
diffText: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Code Review Types
|
|
87
|
+
export type CodeAnnotationType = 'comment' | 'suggestion' | 'concern';
|
|
88
|
+
// 'general' is a review-level comment tied to no file and no line. For 'general'
|
|
89
|
+
// (and the file-less case) filePath is "" and lineStart/lineEnd are 0 — consumers
|
|
90
|
+
// must branch on scope, never read those sentinels as a real path or row.
|
|
91
|
+
export type CodeAnnotationScope = 'line' | 'file' | 'general';
|
|
92
|
+
|
|
93
|
+
/** Conventional Comments label — see https://conventionalcomments.org */
|
|
94
|
+
export type ConventionalLabel =
|
|
95
|
+
| 'praise'
|
|
96
|
+
| 'nitpick'
|
|
97
|
+
| 'suggestion'
|
|
98
|
+
| 'issue'
|
|
99
|
+
| 'todo'
|
|
100
|
+
| 'question'
|
|
101
|
+
| 'thought'
|
|
102
|
+
| 'chore'
|
|
103
|
+
| 'note'
|
|
104
|
+
| 'typo'
|
|
105
|
+
| 'polish'
|
|
106
|
+
| (string & {}); // Allow custom labels while preserving autocomplete for built-ins
|
|
107
|
+
|
|
108
|
+
/** Conventional Comments decoration (parenthesized modifier) */
|
|
109
|
+
export type ConventionalDecoration = 'blocking' | 'non-blocking' | 'if-minor';
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A note attached to a whole PR comment/review/thread (code-review Phase 2).
|
|
113
|
+
* Button-driven (not text-anchored): the reviewer clicks "Annotate" on a card
|
|
114
|
+
* and leaves a note. The comment body travels with it so the agent — which
|
|
115
|
+
* can't see PR discussion — receives the full context on export.
|
|
116
|
+
*/
|
|
117
|
+
export interface CommentAnnotation {
|
|
118
|
+
id: string;
|
|
119
|
+
commentId: string; // the timeline entry id (matches data-comment-id on the card)
|
|
120
|
+
commentAuthor: string;
|
|
121
|
+
commentBody: string;
|
|
122
|
+
text: string; // the reviewer's note
|
|
123
|
+
createdAt: number;
|
|
124
|
+
prUrl?: string; // the PR this note belongs to (see Annotation.prUrl)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface CodeAnnotation {
|
|
128
|
+
id: string;
|
|
129
|
+
type: CodeAnnotationType;
|
|
130
|
+
scope?: CodeAnnotationScope; // Defaults to 'line' for backward compatibility
|
|
131
|
+
filePath: string;
|
|
132
|
+
lineStart: number;
|
|
133
|
+
lineEnd: number;
|
|
134
|
+
side: 'old' | 'new'; // Maps to 'deletions' | 'additions' in @pierre/diffs
|
|
135
|
+
text?: string;
|
|
136
|
+
images?: ImageAttachment[];
|
|
137
|
+
suggestedCode?: string;
|
|
138
|
+
originalCode?: string; // Original selected lines for suggestion diff
|
|
139
|
+
charStart?: number; // Character offset within lineStart (token-level selection)
|
|
140
|
+
charEnd?: number; // Character offset within lineEnd (token-level selection)
|
|
141
|
+
tokenText?: string; // Selected token/span text (token-level selection)
|
|
142
|
+
createdAt: number;
|
|
143
|
+
author?: string;
|
|
144
|
+
source?: string; // External tool identifier (e.g., "eslint") — set when annotation comes from external API
|
|
145
|
+
severity?: 'important' | 'nit' | 'pre_existing'; // Agent review severity (Claude)
|
|
146
|
+
reasoning?: string; // Validation chain — how the issue was confirmed (Claude)
|
|
147
|
+
reviewProfileLabel?: string; // Custom review that produced this finding — shown as a tag
|
|
148
|
+
conventionalLabel?: ConventionalLabel;
|
|
149
|
+
decorations?: ConventionalDecoration[];
|
|
150
|
+
prUrl?: string;
|
|
151
|
+
prNumber?: number;
|
|
152
|
+
prTitle?: string;
|
|
153
|
+
prRepo?: string;
|
|
154
|
+
diffScope?: 'layer' | 'full-stack';
|
|
155
|
+
/** Set when the annotation was created on a commit:<sha> diff (Commits
|
|
156
|
+
* panel). Line numbers anchor to THAT commit's diff-vs-parent — the export
|
|
157
|
+
* labels the annotation with its commit when sent from any other diff, so
|
|
158
|
+
* the agent never reads historical line numbers against the current diff. */
|
|
159
|
+
commitSha?: string;
|
|
160
|
+
/** The commit's one-line subject, captured for readable export labels. */
|
|
161
|
+
commitSubject?: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Token-level metadata passed from selection to annotation creation. */
|
|
165
|
+
export interface TokenAnnotationMeta {
|
|
166
|
+
charStart: number;
|
|
167
|
+
charEnd: number;
|
|
168
|
+
tokenText: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Severity display styles — shared between agent detail panel and inline diff annotations. */
|
|
172
|
+
export const SEVERITY_STYLES: Record<string, { dot: string; label: string }> = {
|
|
173
|
+
important: { dot: 'bg-destructive', label: 'Important' },
|
|
174
|
+
nit: { dot: 'bg-amber-500', label: 'Nit' },
|
|
175
|
+
pre_existing: { dot: 'bg-muted-foreground', label: 'Pre-existing' },
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// For @pierre/diffs integration
|
|
179
|
+
export interface DiffAnnotationMetadata {
|
|
180
|
+
annotationId: string;
|
|
181
|
+
type: CodeAnnotationType;
|
|
182
|
+
text?: string;
|
|
183
|
+
suggestedCode?: string;
|
|
184
|
+
originalCode?: string;
|
|
185
|
+
author?: string;
|
|
186
|
+
severity?: 'important' | 'nit' | 'pre_existing';
|
|
187
|
+
reasoning?: string;
|
|
188
|
+
conventionalLabel?: ConventionalLabel;
|
|
189
|
+
decorations?: ConventionalDecoration[];
|
|
190
|
+
// Shared comment-meta fields (so the inline diff card shows the same identity
|
|
191
|
+
// row — author, time, badges — as the sidebar and file-banner cards).
|
|
192
|
+
createdAt?: number;
|
|
193
|
+
reviewProfileLabel?: string;
|
|
194
|
+
source?: string;
|
|
195
|
+
/** Precomputed clipboard text (location prefix + body + reasoning) so the
|
|
196
|
+
* inline copy action matches the sidebar/banner — the inline card only has
|
|
197
|
+
* the projected metadata, not the full annotation. */
|
|
198
|
+
copyText?: string;
|
|
199
|
+
// AI marker fields (set when kind === 'ai-marker')
|
|
200
|
+
kind?: 'annotation' | 'ai-marker';
|
|
201
|
+
questionId?: string;
|
|
202
|
+
promptPreview?: string;
|
|
203
|
+
hasResponse?: boolean;
|
|
204
|
+
isStreaming?: boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export interface SelectedLineRange {
|
|
208
|
+
start: number;
|
|
209
|
+
end: number;
|
|
210
|
+
side: 'deletions' | 'additions';
|
|
211
|
+
endSide?: 'deletions' | 'additions';
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// AI Chat (inline AI on diffs)
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
export interface AIQuestion {
|
|
219
|
+
id: string;
|
|
220
|
+
prompt: string;
|
|
221
|
+
scope?: {
|
|
222
|
+
kind: 'general' | 'selection';
|
|
223
|
+
label?: string;
|
|
224
|
+
text?: string;
|
|
225
|
+
sourcePath?: string;
|
|
226
|
+
};
|
|
227
|
+
/** undefined = general question (no file scope) */
|
|
228
|
+
filePath?: string;
|
|
229
|
+
/** undefined + filePath present = file-scoped; with filePath = line-scoped */
|
|
230
|
+
lineStart?: number;
|
|
231
|
+
lineEnd?: number;
|
|
232
|
+
side?: 'old' | 'new';
|
|
233
|
+
selectedCode?: string;
|
|
234
|
+
createdAt: number;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export interface AIResponse {
|
|
238
|
+
questionId: string;
|
|
239
|
+
text: string;
|
|
240
|
+
isStreaming: boolean;
|
|
241
|
+
error?: string;
|
|
242
|
+
createdAt: number;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface VaultNode {
|
|
246
|
+
name: string;
|
|
247
|
+
path: string; // relative path within vault
|
|
248
|
+
type: "file" | "folder";
|
|
249
|
+
children?: VaultNode[];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export type { EditorAnnotation } from '@plannotator/core/types';
|
|
253
|
+
|
|
254
|
+
export type {
|
|
255
|
+
ExternalAnnotationEvent,
|
|
256
|
+
} from '@plannotator/core/external-annotation';
|
|
257
|
+
|
|
258
|
+
export type {
|
|
259
|
+
AgentJobInfo,
|
|
260
|
+
AgentJobEvent,
|
|
261
|
+
AgentJobStatus,
|
|
262
|
+
AgentCapability,
|
|
263
|
+
AgentCapabilities,
|
|
264
|
+
} from '@plannotator/core/agent-jobs';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Switch Settings Utility
|
|
3
|
+
*
|
|
4
|
+
* Manages settings for automatic agent switching after plan approval.
|
|
5
|
+
* Supports built-in agents (build), disabled, or custom agent names.
|
|
6
|
+
*
|
|
7
|
+
* Uses cookies (not localStorage) because each hook invocation runs on a
|
|
8
|
+
* random port, and localStorage is scoped by origin including port.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { storage } from './storage';
|
|
12
|
+
|
|
13
|
+
const STORAGE_KEY = 'plannotator-agent-switch';
|
|
14
|
+
const CUSTOM_NAME_KEY = 'plannotator-agent-custom';
|
|
15
|
+
|
|
16
|
+
// AgentSwitchOption is now a string to support dynamic agent names from OpenCode
|
|
17
|
+
export type AgentSwitchOption = string;
|
|
18
|
+
|
|
19
|
+
export interface AgentSwitchSettings {
|
|
20
|
+
switchTo: AgentSwitchOption;
|
|
21
|
+
customName?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Fallback options when API is unavailable or for non-OpenCode origins
|
|
25
|
+
export const AGENT_OPTIONS: { value: string; label: string; description: string }[] = [
|
|
26
|
+
{ value: 'build', label: 'Build', description: 'Switch to build agent after approval' },
|
|
27
|
+
{ value: 'custom', label: 'Custom', description: 'Switch to a custom agent after approval' },
|
|
28
|
+
{ value: 'disabled', label: 'Disabled', description: 'Stay on current agent after approval' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const DEFAULT_SETTINGS: AgentSwitchSettings = {
|
|
32
|
+
switchTo: 'build',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get current agent switch settings from storage
|
|
37
|
+
*/
|
|
38
|
+
export function getAgentSwitchSettings(): AgentSwitchSettings {
|
|
39
|
+
const stored = storage.getItem(STORAGE_KEY);
|
|
40
|
+
const customName = storage.getItem(CUSTOM_NAME_KEY) || undefined;
|
|
41
|
+
|
|
42
|
+
// Accept any non-empty string (supports dynamic agent names from OpenCode)
|
|
43
|
+
if (stored) {
|
|
44
|
+
return { switchTo: stored, customName };
|
|
45
|
+
}
|
|
46
|
+
return DEFAULT_SETTINGS;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Save agent switch settings to storage
|
|
51
|
+
*/
|
|
52
|
+
export function saveAgentSwitchSettings(settings: AgentSwitchSettings): void {
|
|
53
|
+
storage.setItem(STORAGE_KEY, settings.switchTo);
|
|
54
|
+
if (settings.customName) {
|
|
55
|
+
storage.setItem(CUSTOM_NAME_KEY, settings.customName);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get the effective agent name for switching
|
|
61
|
+
* Returns undefined if disabled, otherwise returns the agent name
|
|
62
|
+
*/
|
|
63
|
+
export function getEffectiveAgentName(settings: AgentSwitchSettings): string | undefined {
|
|
64
|
+
if (settings.switchTo === 'disabled') {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
if (settings.switchTo === 'custom' && settings.customName) {
|
|
68
|
+
return settings.customName;
|
|
69
|
+
}
|
|
70
|
+
return settings.switchTo; // 'build' or fallback
|
|
71
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createElement, type ReactNode } from 'react';
|
|
2
|
+
import { marked } from 'marked';
|
|
3
|
+
import DOMPurify from 'dompurify';
|
|
4
|
+
|
|
5
|
+
export function renderChatMarkdown(text: string): ReactNode {
|
|
6
|
+
const html = marked.parse(text, { async: false, breaks: true }) as string;
|
|
7
|
+
const clean = DOMPurify.sanitize(html, {
|
|
8
|
+
ALLOWED_TAGS: [
|
|
9
|
+
'p', 'br', 'strong', 'em', 'code', 'pre', 'ul', 'ol', 'li',
|
|
10
|
+
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'a', 'blockquote',
|
|
11
|
+
],
|
|
12
|
+
ALLOWED_ATTR: ['href', 'rel', 'class'],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return createElement('div', {
|
|
16
|
+
className: 'ai-markdown',
|
|
17
|
+
dangerouslySetInnerHTML: { __html: clean },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatRelativeTime(ts: number): string {
|
|
22
|
+
const seconds = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
23
|
+
const minutes = Math.floor(seconds / 60);
|
|
24
|
+
const hours = Math.floor(minutes / 60);
|
|
25
|
+
const days = Math.floor(hours / 24);
|
|
26
|
+
|
|
27
|
+
if (seconds < 60) return 'now';
|
|
28
|
+
if (minutes < 60) return `${minutes}m`;
|
|
29
|
+
if (hours < 24) return `${hours}h`;
|
|
30
|
+
if (days < 7) return `${days}d`;
|
|
31
|
+
|
|
32
|
+
return new Date(ts).toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
|
|
33
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose the per-message "changes under review" preamble for Ask AI.
|
|
3
|
+
*
|
|
4
|
+
* `reviewContext` is the server-built changeset description (from the shared
|
|
5
|
+
* agent-review prompt machine — the same text the launchable review jobs get).
|
|
6
|
+
* It is either a short git command to run, or a pasted diff for view modes git
|
|
7
|
+
* can't reproduce (stacked-PR full-stack, hide-whitespace, untracked, workspace).
|
|
8
|
+
*
|
|
9
|
+
* On the first message — and whenever the user switches what they're viewing
|
|
10
|
+
* mid-chat, so the context string changes — we inject the full block so the
|
|
11
|
+
* agent always sees exactly the on-screen changeset. On an unchanged follow-up
|
|
12
|
+
* we avoid re-pasting a large diff (the conversation history already carries it)
|
|
13
|
+
* and emit only a short reminder; a short command-based context is cheap enough
|
|
14
|
+
* to restate every turn, which keeps the agent oriented.
|
|
15
|
+
*/
|
|
16
|
+
export function buildReviewContextPreamble(
|
|
17
|
+
reviewContext: string | null | undefined,
|
|
18
|
+
opts: { changed: boolean },
|
|
19
|
+
): string {
|
|
20
|
+
const ctx = reviewContext?.trim();
|
|
21
|
+
if (!ctx) return '';
|
|
22
|
+
if (opts.changed) return ctx;
|
|
23
|
+
// Unchanged follow-up. A pasted diff (fenced code block) must NOT be repeated
|
|
24
|
+
// every turn; a command-based context is short, so restating it is fine.
|
|
25
|
+
const isPasted = ctx.includes('```');
|
|
26
|
+
return isPasted
|
|
27
|
+
? '[Still reviewing the same changes shown earlier in this conversation.]'
|
|
28
|
+
: ctx;
|
|
29
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Provider Settings Utility
|
|
3
|
+
*
|
|
4
|
+
* Manages the user's default AI provider and per-provider model preferences.
|
|
5
|
+
* Uses cookies (not localStorage) because each hook invocation runs on a
|
|
6
|
+
* random port, and localStorage is scoped by origin including port.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { storage } from './storage';
|
|
10
|
+
import { AGENT_CONFIG, getAgentAIProviderTypes, type Origin } from '@plannotator/core/agents';
|
|
11
|
+
|
|
12
|
+
const PROVIDER_KEY = 'plannotator-ai-provider';
|
|
13
|
+
const MODELS_KEY = 'plannotator-ai-models';
|
|
14
|
+
const PROVIDER_BY_ORIGIN_KEY = 'plannotator-ai-provider-by-origin';
|
|
15
|
+
|
|
16
|
+
export interface AIProviderModel {
|
|
17
|
+
id: string;
|
|
18
|
+
label: string;
|
|
19
|
+
default?: boolean;
|
|
20
|
+
/** Reasoning-effort options this model supports (provider-reported, e.g. Codex). */
|
|
21
|
+
reasoningEfforts?: { id: string; label: string }[];
|
|
22
|
+
/** The model's default reasoning effort. */
|
|
23
|
+
defaultReasoningEffort?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface AIProviderOption {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
models?: AIProviderModel[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AIProviderSettings {
|
|
33
|
+
/** The provider instance ID to use, or null for server default. */
|
|
34
|
+
providerId: string | null;
|
|
35
|
+
/** Preferred model per provider. Key = provider instance ID, value = model ID. */
|
|
36
|
+
preferredModels: Record<string, string>;
|
|
37
|
+
/** Preferred provider per detected agent origin. Key = Origin, value = provider instance ID. */
|
|
38
|
+
providerByOrigin: Partial<Record<Origin, string>>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AIProviderSelection {
|
|
42
|
+
providerId: string | null;
|
|
43
|
+
model: string | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function originHasDedicatedAIProvider(origin: Origin | null | undefined): boolean {
|
|
47
|
+
return getAgentAIProviderTypes(origin).length > 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get current AI provider settings from storage
|
|
52
|
+
*/
|
|
53
|
+
export function getAIProviderSettings(): AIProviderSettings {
|
|
54
|
+
const providerId = storage.getItem(PROVIDER_KEY) || null;
|
|
55
|
+
let preferredModels: Record<string, string> = {};
|
|
56
|
+
let providerByOrigin: Partial<Record<Origin, string>> = {};
|
|
57
|
+
try {
|
|
58
|
+
const raw = storage.getItem(MODELS_KEY);
|
|
59
|
+
if (raw) preferredModels = JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
// Invalid JSON — start fresh
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const raw = storage.getItem(PROVIDER_BY_ORIGIN_KEY);
|
|
65
|
+
if (raw) {
|
|
66
|
+
const parsed = JSON.parse(raw);
|
|
67
|
+
if (parsed && typeof parsed === 'object') {
|
|
68
|
+
for (const [origin, value] of Object.entries(parsed)) {
|
|
69
|
+
if (origin in AGENT_CONFIG && typeof value === 'string') {
|
|
70
|
+
providerByOrigin[origin as Origin] = value;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// Invalid JSON — start fresh
|
|
77
|
+
}
|
|
78
|
+
return { providerId, preferredModels, providerByOrigin };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Save AI provider settings to storage
|
|
83
|
+
*/
|
|
84
|
+
export function saveAIProviderSettings(settings: AIProviderSettings): void {
|
|
85
|
+
if (settings.providerId) {
|
|
86
|
+
storage.setItem(PROVIDER_KEY, settings.providerId);
|
|
87
|
+
} else {
|
|
88
|
+
storage.removeItem(PROVIDER_KEY);
|
|
89
|
+
}
|
|
90
|
+
storage.setItem(MODELS_KEY, JSON.stringify(settings.preferredModels));
|
|
91
|
+
const providerByOrigin = settings.providerByOrigin ?? {};
|
|
92
|
+
if (Object.keys(providerByOrigin).length > 0) {
|
|
93
|
+
storage.setItem(PROVIDER_BY_ORIGIN_KEY, JSON.stringify(providerByOrigin));
|
|
94
|
+
} else {
|
|
95
|
+
storage.removeItem(PROVIDER_BY_ORIGIN_KEY);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get the preferred model for a specific provider
|
|
101
|
+
*/
|
|
102
|
+
export function getPreferredModel(providerId: string): string | null {
|
|
103
|
+
const { preferredModels } = getAIProviderSettings();
|
|
104
|
+
return preferredModels[providerId] ?? null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Save the preferred model for a specific provider (without changing other preferences)
|
|
109
|
+
*/
|
|
110
|
+
export function savePreferredModel(providerId: string, modelId: string): void {
|
|
111
|
+
const settings = getAIProviderSettings();
|
|
112
|
+
settings.preferredModels[providerId] = modelId;
|
|
113
|
+
saveAIProviderSettings(settings);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Find the first available provider that naturally matches the current origin.
|
|
118
|
+
* Instance IDs can be custom, so we match both the registry ID and provider type.
|
|
119
|
+
*/
|
|
120
|
+
export function findOriginAIProvider(
|
|
121
|
+
providers: AIProviderOption[],
|
|
122
|
+
origin: Origin | null | undefined,
|
|
123
|
+
): AIProviderOption | null {
|
|
124
|
+
const providerTypes = getAgentAIProviderTypes(origin);
|
|
125
|
+
for (const providerType of providerTypes) {
|
|
126
|
+
const provider = providers.find(p => p.id === providerType || p.name === providerType);
|
|
127
|
+
if (provider) return provider;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function resolveAIModelForProvider(
|
|
133
|
+
provider: AIProviderOption | null | undefined,
|
|
134
|
+
preferredModels: Record<string, string>,
|
|
135
|
+
): string | null {
|
|
136
|
+
if (!provider) return null;
|
|
137
|
+
const models = provider.models ?? [];
|
|
138
|
+
const modelIds = new Set(models.map(m => m.id));
|
|
139
|
+
const preferredModel = preferredModels[provider.id];
|
|
140
|
+
if (preferredModel && (modelIds.size === 0 || modelIds.has(preferredModel))) {
|
|
141
|
+
return preferredModel;
|
|
142
|
+
}
|
|
143
|
+
const defaultModel = models.find(m => m.default) ?? models[0];
|
|
144
|
+
return defaultModel?.id ?? null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function resolveAIProviderSelection(options: {
|
|
148
|
+
providers: AIProviderOption[];
|
|
149
|
+
origin?: Origin | null;
|
|
150
|
+
settings?: AIProviderSettings;
|
|
151
|
+
serverDefaultProvider?: string | null;
|
|
152
|
+
}): AIProviderSelection {
|
|
153
|
+
const { providers, origin, serverDefaultProvider } = options;
|
|
154
|
+
const settings = options.settings ?? getAIProviderSettings();
|
|
155
|
+
if (providers.length === 0) return { providerId: null, model: null };
|
|
156
|
+
|
|
157
|
+
const byId = (id: string | null | undefined) =>
|
|
158
|
+
id ? providers.find(provider => provider.id === id) ?? null : null;
|
|
159
|
+
|
|
160
|
+
const provider =
|
|
161
|
+
byId(origin ? settings.providerByOrigin[origin] : null) ??
|
|
162
|
+
findOriginAIProvider(providers, origin) ??
|
|
163
|
+
byId(settings.providerId) ??
|
|
164
|
+
byId(serverDefaultProvider) ??
|
|
165
|
+
providers[0] ??
|
|
166
|
+
null;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
providerId: provider?.id ?? null,
|
|
170
|
+
model: resolveAIModelForProvider(provider, settings.preferredModels),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function saveAIProviderSelection(options: {
|
|
175
|
+
providerId: string | null;
|
|
176
|
+
model?: string | null;
|
|
177
|
+
origin?: Origin | null;
|
|
178
|
+
settings?: AIProviderSettings;
|
|
179
|
+
}): void {
|
|
180
|
+
const settings = options.settings ?? getAIProviderSettings();
|
|
181
|
+
saveAIProviderSettings(applyAIProviderSelection(settings, options));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function applyAIProviderSelection(
|
|
185
|
+
settings: AIProviderSettings,
|
|
186
|
+
options: {
|
|
187
|
+
providerId: string | null;
|
|
188
|
+
model?: string | null;
|
|
189
|
+
origin?: Origin | null;
|
|
190
|
+
},
|
|
191
|
+
): AIProviderSettings {
|
|
192
|
+
const preferredModels = { ...settings.preferredModels };
|
|
193
|
+
if (options.providerId && options.model) {
|
|
194
|
+
preferredModels[options.providerId] = options.model;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const providerByOrigin = { ...settings.providerByOrigin };
|
|
198
|
+
let providerId = settings.providerId;
|
|
199
|
+
const hasOriginDefault = originHasDedicatedAIProvider(options.origin);
|
|
200
|
+
|
|
201
|
+
if (options.origin && hasOriginDefault) {
|
|
202
|
+
if (options.providerId) {
|
|
203
|
+
providerByOrigin[options.origin] = options.providerId;
|
|
204
|
+
} else {
|
|
205
|
+
delete providerByOrigin[options.origin];
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
providerId = options.providerId;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
...settings,
|
|
213
|
+
providerId,
|
|
214
|
+
preferredModels,
|
|
215
|
+
providerByOrigin,
|
|
216
|
+
};
|
|
217
|
+
}
|
package/utils/anchors.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function decodeAnchorHash(hash: string): string | null {
|
|
2
|
+
const raw = hash.replace(/^#/, '').split('?')[0]?.trim();
|
|
3
|
+
if (!raw) return null;
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
const decoded = decodeURIComponent(raw).trim();
|
|
7
|
+
return decoded || null;
|
|
8
|
+
} catch {
|
|
9
|
+
return raw;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentTerminalAgent } from "@plannotator/core/agent-terminal";
|
|
2
|
+
import { storage } from "./storage";
|
|
3
|
+
|
|
4
|
+
const DEFAULT_AGENT_KEY = "plannotator-annotate-agent-terminal-default";
|
|
5
|
+
|
|
6
|
+
export function getSavedAnnotateAgentId(): string | null {
|
|
7
|
+
return storage.getItem(DEFAULT_AGENT_KEY);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function saveAnnotateAgentId(agentId: string): void {
|
|
11
|
+
storage.setItem(DEFAULT_AGENT_KEY, agentId);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function resolveAnnotateAgentId(
|
|
15
|
+
agents: AgentTerminalAgent[],
|
|
16
|
+
savedAgentId: string | null,
|
|
17
|
+
): string {
|
|
18
|
+
const availableAgents = agents.filter((agent) => agent.available);
|
|
19
|
+
if (savedAgentId && availableAgents.some((agent) => agent.id === savedAgentId)) {
|
|
20
|
+
return savedAgentId;
|
|
21
|
+
}
|
|
22
|
+
return availableAgents[0]?.id ?? "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveAgentTerminalWebSocketUrl(path: string): string {
|
|
26
|
+
const url = new URL(path, window.location.href);
|
|
27
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
28
|
+
return url.toString();
|
|
29
|
+
}
|