@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.
Files changed (305) hide show
  1. package/README.md +60 -0
  2. package/assets/diff-options.png +0 -0
  3. package/assets/icon-codex.png +0 -0
  4. package/assets/look-flat.png +0 -0
  5. package/assets/look-grid.png +0 -0
  6. package/assets/review-sections.png +0 -0
  7. package/assets/review-tree.png +0 -0
  8. package/assets/workspaces.webp +0 -0
  9. package/components/AISettingsTab.tsx +145 -0
  10. package/components/ActionMenu.tsx +101 -0
  11. package/components/AgentControls.tsx +216 -0
  12. package/components/AgentsTab.tsx +1294 -0
  13. package/components/AnnotationPanel.tsx +731 -0
  14. package/components/AnnotationSidebar.tsx +86 -0
  15. package/components/AnnotationToolbar.tsx +323 -0
  16. package/components/AnnotationToolstrip.tsx +355 -0
  17. package/components/ApproveDropdown.tsx +170 -0
  18. package/components/AttachmentsButton.tsx +410 -0
  19. package/components/BlockRenderer.tsx +163 -0
  20. package/components/BorderTrail.tsx +38 -0
  21. package/components/CodeFilePicker.tsx +64 -0
  22. package/components/CodeFilePopout.tsx +602 -0
  23. package/components/CodePathValidationContext.tsx +14 -0
  24. package/components/CommentPopover.tsx +512 -0
  25. package/components/CompletionOverlay.tsx +99 -0
  26. package/components/ConfirmDialog.tsx +128 -0
  27. package/components/DocBadges.tsx +231 -0
  28. package/components/EditorAnnotationCard.tsx +65 -0
  29. package/components/ExportModal.tsx +565 -0
  30. package/components/FloatingQuickLabelPicker.tsx +144 -0
  31. package/components/GitHubIcon.tsx +16 -0
  32. package/components/GitLabIcon.tsx +30 -0
  33. package/components/GraphvizBlock.tsx +511 -0
  34. package/components/ImageAnnotator/Canvas.tsx +118 -0
  35. package/components/ImageAnnotator/Toolbar.tsx +218 -0
  36. package/components/ImageAnnotator/index.tsx +272 -0
  37. package/components/ImageAnnotator/types.ts +39 -0
  38. package/components/ImageAnnotator/utils.ts +147 -0
  39. package/components/ImageThumbnail.tsx +126 -0
  40. package/components/ImportModal.tsx +146 -0
  41. package/components/InlineMarkdown.tsx +1094 -0
  42. package/components/KeyboardShortcuts.tsx +208 -0
  43. package/components/Landing.tsx +496 -0
  44. package/components/ListItemBody.tsx +62 -0
  45. package/components/ListMarker.tsx +73 -0
  46. package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
  47. package/components/MarkdownEditor.tsx +44 -0
  48. package/components/MenuVersionSection.tsx +91 -0
  49. package/components/MermaidBlock.tsx +579 -0
  50. package/components/ModeToggle.tsx +68 -0
  51. package/components/OpenInAppButton.tsx +299 -0
  52. package/components/OverlayScrollArea.tsx +82 -0
  53. package/components/PermissionModeSetup.tsx +92 -0
  54. package/components/PinpointOverlay.tsx +99 -0
  55. package/components/PlanAIAnnouncementDialog.tsx +187 -0
  56. package/components/PlanHeaderMenu.tsx +299 -0
  57. package/components/PopoutDialog.tsx +91 -0
  58. package/components/Popover.tsx +27 -0
  59. package/components/ProviderIcons.tsx +51 -0
  60. package/components/PullRequestIcon.tsx +12 -0
  61. package/components/QuickLabelDropdown.tsx +63 -0
  62. package/components/RenderedMarkdown.tsx +54 -0
  63. package/components/RepoIcon.tsx +12 -0
  64. package/components/ResizeHandle.tsx +86 -0
  65. package/components/ReviewAgentsIcon.tsx +18 -0
  66. package/components/SearchableSelect.tsx +157 -0
  67. package/components/Settings.tsx +2220 -0
  68. package/components/SparklesIcon.tsx +45 -0
  69. package/components/StickyHeaderLane.tsx +260 -0
  70. package/components/TableOfContents.tsx +157 -0
  71. package/components/TaterSpritePullup.tsx +34 -0
  72. package/components/TaterSpriteRunning.tsx +54 -0
  73. package/components/TaterSpriteSitting.tsx +37 -0
  74. package/components/TextShimmer.tsx +57 -0
  75. package/components/ThemeProvider.tsx +162 -0
  76. package/components/ThemeTab.tsx +139 -0
  77. package/components/ToolbarButtons.tsx +116 -0
  78. package/components/Tooltip.tsx +45 -0
  79. package/components/Viewer.tsx +977 -0
  80. package/components/ai/AIProviderBar.tsx +95 -0
  81. package/components/ai/DocumentAIChatPanel.tsx +313 -0
  82. package/components/blocks/AlertBlock.tsx +58 -0
  83. package/components/blocks/Callout.tsx +60 -0
  84. package/components/blocks/CodeBlock.tsx +75 -0
  85. package/components/blocks/HtmlBlock.tsx +122 -0
  86. package/components/blocks/MathBlock.tsx +36 -0
  87. package/components/blocks/TableBlock.tsx +146 -0
  88. package/components/blocks/TablePopout.tsx +279 -0
  89. package/components/blocks/TableToolbar.tsx +153 -0
  90. package/components/blocks/proseBody.tsx +102 -0
  91. package/components/core/button.tsx +44 -0
  92. package/components/core/textarea.tsx +26 -0
  93. package/components/diagramLanguages.ts +14 -0
  94. package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
  95. package/components/html-viewer/HtmlViewer.tsx +386 -0
  96. package/components/html-viewer/bridge-script.ts +505 -0
  97. package/components/html-viewer/index.ts +1 -0
  98. package/components/html-viewer/useHtmlAnnotation.ts +400 -0
  99. package/components/icons/AgentIcons.tsx +66 -0
  100. package/components/icons/AppIcon.tsx +56 -0
  101. package/components/icons/MessagesIcon.tsx +11 -0
  102. package/components/icons/ObsidianIcons.tsx +208 -0
  103. package/components/icons/app/android-studio.svg +369 -0
  104. package/components/icons/app/antigravity.svg +97 -0
  105. package/components/icons/app/cursor.svg +16 -0
  106. package/components/icons/app/file-explorer.svg +20 -0
  107. package/components/icons/app/finder.png +0 -0
  108. package/components/icons/app/ghostty.svg +13 -0
  109. package/components/icons/app/iterm2.svg +13 -0
  110. package/components/icons/app/powershell.svg +14 -0
  111. package/components/icons/app/sublime-text.svg +17 -0
  112. package/components/icons/app/terminal.png +0 -0
  113. package/components/icons/app/textmate.png +0 -0
  114. package/components/icons/app/vscode.svg +39 -0
  115. package/components/icons/app/warp.png +0 -0
  116. package/components/icons/app/xcode.png +0 -0
  117. package/components/icons/app/zed-dark.svg +15 -0
  118. package/components/icons/app/zed.svg +15 -0
  119. package/components/icons/themeIcons.tsx +47 -0
  120. package/components/mermaidSvg.ts +33 -0
  121. package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
  122. package/components/plan-diff/PlanDiffBadge.tsx +48 -0
  123. package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
  124. package/components/plan-diff/PlanDiffViewer.tsx +214 -0
  125. package/components/plan-diff/PlanRawDiffView.tsx +102 -0
  126. package/components/plan-diff/VSCodeIcon.tsx +132 -0
  127. package/components/settings/HooksTab.tsx +208 -0
  128. package/components/sidebar/ArchiveBrowser.tsx +98 -0
  129. package/components/sidebar/CountBadge.tsx +12 -0
  130. package/components/sidebar/FileBrowser.tsx +498 -0
  131. package/components/sidebar/MessagesBrowser.tsx +109 -0
  132. package/components/sidebar/SidebarContainer.tsx +353 -0
  133. package/components/sidebar/SidebarTabs.tsx +155 -0
  134. package/components/sidebar/VersionBrowser.tsx +141 -0
  135. package/components/types.d.ts +7 -0
  136. package/components/ui/badge.tsx +44 -0
  137. package/components/ui/button.tsx +83 -0
  138. package/components/ui/card.tsx +58 -0
  139. package/components/ui/dialog.tsx +106 -0
  140. package/components/ui/dropdown-menu.tsx +240 -0
  141. package/components/ui/state-pill.tsx +45 -0
  142. package/components/ui/tabs.tsx +46 -0
  143. package/components/ui/textarea.tsx +25 -0
  144. package/config/configStore.ts +214 -0
  145. package/config/index.ts +4 -0
  146. package/config/reviewView.ts +42 -0
  147. package/config/settings.ts +310 -0
  148. package/config/useConfig.ts +20 -0
  149. package/configure.ts +69 -0
  150. package/globals.d.ts +14 -0
  151. package/hooks/pfm/useCodeFilePopout.ts +111 -0
  152. package/hooks/useAIChat.ts +532 -0
  153. package/hooks/useAIProviderConfig.ts +115 -0
  154. package/hooks/useActiveSection.ts +78 -0
  155. package/hooks/useAgentJobs.ts +306 -0
  156. package/hooks/useAgentSettings.ts +579 -0
  157. package/hooks/useAgents.ts +90 -0
  158. package/hooks/useAnnotationDraft.ts +523 -0
  159. package/hooks/useAnnotationHighlighter.ts +1170 -0
  160. package/hooks/useArchive.ts +171 -0
  161. package/hooks/useAutoClose.ts +104 -0
  162. package/hooks/useCodeAnnotationDraft.ts +194 -0
  163. package/hooks/useCodeFilePopout.ts +1 -0
  164. package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
  165. package/hooks/useDraggable.ts +108 -0
  166. package/hooks/useEditorAnnotations.ts +64 -0
  167. package/hooks/useExternalAnnotationHighlights.ts +105 -0
  168. package/hooks/useExternalAnnotations.ts +268 -0
  169. package/hooks/useFileBrowser.ts +402 -0
  170. package/hooks/useInputMethodSwitch.ts +89 -0
  171. package/hooks/useIsMobile.ts +17 -0
  172. package/hooks/useLinkedDoc.ts +494 -0
  173. package/hooks/useOverlayViewport.ts +36 -0
  174. package/hooks/usePinpoint.ts +182 -0
  175. package/hooks/usePlanDiff.ts +176 -0
  176. package/hooks/usePrintMode.ts +27 -0
  177. package/hooks/useResizablePanel.ts +173 -0
  178. package/hooks/useScrollViewport.ts +38 -0
  179. package/hooks/useSharing.ts +441 -0
  180. package/hooks/useSidebar.ts +55 -0
  181. package/hooks/useUpdateCheck.ts +132 -0
  182. package/hooks/useValidatedCodePaths.ts +94 -0
  183. package/icons/GitUser.tsx +17 -0
  184. package/lib/utils.ts +11 -0
  185. package/package.json +117 -0
  186. package/plannotator.webp +0 -0
  187. package/print.css +427 -0
  188. package/shortcuts/code-review/ai.shortcuts.ts +25 -0
  189. package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
  190. package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
  191. package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
  192. package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
  193. package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
  194. package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
  195. package/shortcuts/core.ts +399 -0
  196. package/shortcuts/index.ts +21 -0
  197. package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
  198. package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
  199. package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
  200. package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
  201. package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
  202. package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
  203. package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
  204. package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
  205. package/shortcuts/runtime.ts +259 -0
  206. package/sprite_package_additional/index.html +34 -0
  207. package/sprite_package_additional/sprite.png +0 -0
  208. package/sprite_package_new/index.html +34 -0
  209. package/sprite_package_new/sprite.png +0 -0
  210. package/sprite_package_pulluphang/index.html +34 -0
  211. package/sprite_package_pulluphang/sprite.png +0 -0
  212. package/styles.css +1 -0
  213. package/theme.css +893 -0
  214. package/themes/adwaita.css +62 -0
  215. package/themes/andromeeda.css +66 -0
  216. package/themes/aurora-x.css +66 -0
  217. package/themes/ayu-dark.css +66 -0
  218. package/themes/caffeine.css +113 -0
  219. package/themes/catppuccin.css +62 -0
  220. package/themes/claude-plus.css +60 -0
  221. package/themes/cursor-hc.css +34 -0
  222. package/themes/cursor-midnight.css +34 -0
  223. package/themes/cursor.css +62 -0
  224. package/themes/dark-plus.css +66 -0
  225. package/themes/doom-64.css +109 -0
  226. package/themes/dracula.css +33 -0
  227. package/themes/everforest-hard.css +62 -0
  228. package/themes/everforest-soft.css +62 -0
  229. package/themes/everforest.css +62 -0
  230. package/themes/github.css +66 -0
  231. package/themes/gruvbox.css +62 -0
  232. package/themes/houston.css +66 -0
  233. package/themes/kanagawa-dragon.css +34 -0
  234. package/themes/kanagawa-lotus.css +34 -0
  235. package/themes/kanagawa-wave.css +34 -0
  236. package/themes/laserwave.css +66 -0
  237. package/themes/material.css +62 -0
  238. package/themes/min.css +66 -0
  239. package/themes/monokai-pro.css +34 -0
  240. package/themes/neutral.css +59 -0
  241. package/themes/night-owl.css +66 -0
  242. package/themes/nord.css +66 -0
  243. package/themes/one-dark-pro.css +66 -0
  244. package/themes/one-light.css +66 -0
  245. package/themes/paulmillr.css +34 -0
  246. package/themes/plannotator.css +60 -0
  247. package/themes/plastic.css +66 -0
  248. package/themes/poimandres.css +66 -0
  249. package/themes/quantum-rose.css +109 -0
  250. package/themes/red.css +66 -0
  251. package/themes/rose-pine.css +62 -0
  252. package/themes/simple.css +124 -0
  253. package/themes/slack.css +66 -0
  254. package/themes/snazzy-light.css +66 -0
  255. package/themes/soft-pop.css +60 -0
  256. package/themes/solar-dusk.css +109 -0
  257. package/themes/solarized.css +66 -0
  258. package/themes/synthwave-84.css +34 -0
  259. package/themes/terminal.css +62 -0
  260. package/themes/tinacious.css +57 -0
  261. package/themes/tokyo-night.css +62 -0
  262. package/themes/vesper.css +62 -0
  263. package/themes/vitesse-black.css +66 -0
  264. package/themes/vitesse.css +62 -0
  265. package/types.ts +264 -0
  266. package/utils/agentSwitch.ts +71 -0
  267. package/utils/aiChatFormat.ts +33 -0
  268. package/utils/aiPrompt.ts +29 -0
  269. package/utils/aiProvider.ts +217 -0
  270. package/utils/anchors.ts +11 -0
  271. package/utils/annotateAgentTerminal.ts +29 -0
  272. package/utils/annotationHelpers.ts +101 -0
  273. package/utils/bear.ts +59 -0
  274. package/utils/blockTargeting.ts +240 -0
  275. package/utils/callback.ts +99 -0
  276. package/utils/commentContent.ts +8 -0
  277. package/utils/defaultNotesApp.ts +20 -0
  278. package/utils/diffFonts.ts +33 -0
  279. package/utils/editorMode.ts +31 -0
  280. package/utils/fileBrowser.ts +40 -0
  281. package/utils/generateId.ts +7 -0
  282. package/utils/generateIdentity.ts +29 -0
  283. package/utils/identity.ts +112 -0
  284. package/utils/inlineTransforms.ts +41 -0
  285. package/utils/inputMethod.ts +17 -0
  286. package/utils/lookAndFeelAnnouncement.ts +18 -0
  287. package/utils/obsidian.ts +201 -0
  288. package/utils/octarine.ts +53 -0
  289. package/utils/parser.ts +991 -0
  290. package/utils/permissionMode.ts +77 -0
  291. package/utils/planAIAnnouncement.ts +17 -0
  292. package/utils/planAgentInstructions.ts +137 -0
  293. package/utils/planDiffEngine.ts +590 -0
  294. package/utils/planSave.ts +49 -0
  295. package/utils/platform.ts +11 -0
  296. package/utils/quickLabels.ts +77 -0
  297. package/utils/reviewAgentInstructions.ts +182 -0
  298. package/utils/sanitizeHtml.ts +45 -0
  299. package/utils/sharing.ts +350 -0
  300. package/utils/slugify.ts +37 -0
  301. package/utils/storage.ts +141 -0
  302. package/utils/themeRegistry.ts +567 -0
  303. package/utils/uiPreferences.ts +34 -0
  304. package/utils/upload.ts +56 -0
  305. package/utils/wideMode.ts +48 -0
@@ -0,0 +1,187 @@
1
+ import React from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import type { Origin } from '@plannotator/core/agents';
4
+ import { AGENT_CONFIG, getAgentAIProviderTypes, getAgentName } from '@plannotator/core/agents';
5
+ import { SparklesIcon } from './SparklesIcon';
6
+ import { getProviderMeta } from './ProviderIcons';
7
+
8
+ interface PlanAIAnnouncementProvider {
9
+ id: string;
10
+ name: string;
11
+ }
12
+
13
+ interface PlanAIAnnouncementDialogProps {
14
+ isOpen: boolean;
15
+ origin?: Origin | null;
16
+ providerName?: string | null;
17
+ /** Actually-detected providers (installed + authenticated). Cards matching one of these are selectable. */
18
+ providers?: PlanAIAnnouncementProvider[];
19
+ onSelectProvider?: (providerId: string) => void;
20
+ onOpenAI: () => void;
21
+ onDismiss: () => void;
22
+ }
23
+
24
+ const SUPPORTED_AI_PROVIDER_TYPES = Array.from(
25
+ new Set(
26
+ (Object.keys(AGENT_CONFIG) as Origin[])
27
+ .flatMap(agentOrigin => getAgentAIProviderTypes(agentOrigin))
28
+ )
29
+ );
30
+
31
+ const UNSUPPORTED_AI_ORIGINS = (Object.keys(AGENT_CONFIG) as Origin[])
32
+ .filter(agentOrigin => getAgentAIProviderTypes(agentOrigin).length === 0);
33
+
34
+ export const PlanAIAnnouncementDialog: React.FC<PlanAIAnnouncementDialogProps> = ({
35
+ isOpen,
36
+ origin,
37
+ providerName,
38
+ providers = [],
39
+ onSelectProvider,
40
+ onOpenAI,
41
+ onDismiss,
42
+ }) => {
43
+ if (!isOpen) return null;
44
+
45
+ const agentLabel = getAgentName(origin ?? undefined);
46
+ const providerLabel = providerName ? getProviderMeta(providerName).label : null;
47
+ const unsupportedLabels = UNSUPPORTED_AI_ORIGINS.map(getAgentName).join(', ');
48
+
49
+ return createPortal(
50
+ <div className="fixed inset-0 z-[100] flex items-center justify-center bg-background/90 backdrop-blur-sm p-4">
51
+ <div className="bg-card border border-border rounded-xl w-full max-w-xl shadow-2xl">
52
+ {/* Header */}
53
+ <div className="p-5 border-b border-border">
54
+ <div className="flex items-center gap-2 mb-2">
55
+ <div className="p-1.5 rounded-lg bg-primary/15">
56
+ <SparklesIcon className="w-5 h-5 text-primary" />
57
+ </div>
58
+ <h3 className="font-semibold text-base">New: Ask AI for annotated documents</h3>
59
+ </div>
60
+ <p className="text-sm text-muted-foreground">
61
+ Chat with plans and annotated documents directly inside Plannotator.
62
+ </p>
63
+ {providerLabel && (
64
+ <p className="text-xs text-muted-foreground/70 mt-1">
65
+ Plannotator selected {providerLabel} for {agentLabel}.
66
+ </p>
67
+ )}
68
+ </div>
69
+
70
+ {/* Details */}
71
+ <div className="p-4 space-y-4">
72
+ {SUPPORTED_AI_PROVIDER_TYPES.length > 0 && (
73
+ <div>
74
+ <div className="text-xs text-muted-foreground mb-2">Supported providers</div>
75
+ <div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
76
+ {SUPPORTED_AI_PROVIDER_TYPES.map(providerType => {
77
+ const meta = getProviderMeta(providerType);
78
+ const Icon = meta.icon;
79
+ const isSelected = providerType === providerName;
80
+ // A card is selectable only if the provider is actually detected on this machine.
81
+ const detected = providers.find(p => p.name === providerType) ?? null;
82
+ const isSelectable = Boolean(detected && onSelectProvider);
83
+
84
+ const inner = (
85
+ <>
86
+ <div className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-background/70 text-muted-foreground">
87
+ <Icon className="h-[18px] w-[18px]" />
88
+ </div>
89
+ <div className="min-w-0">
90
+ <div className="truncate text-sm font-medium">{meta.label}</div>
91
+ {isSelected ? (
92
+ <div className="text-[10px] uppercase tracking-wide text-primary">selected</div>
93
+ ) : !detected ? (
94
+ <div className="text-[10px] text-muted-foreground/60">not installed</div>
95
+ ) : null}
96
+ </div>
97
+ </>
98
+ );
99
+
100
+ if (!isSelectable) {
101
+ return (
102
+ <div
103
+ key={providerType}
104
+ className={`flex min-h-16 items-center gap-2 rounded-lg border p-2.5 ${
105
+ isSelected
106
+ ? 'border-primary bg-primary/5'
107
+ : 'border-border bg-muted/35 opacity-60'
108
+ }`}
109
+ >
110
+ {inner}
111
+ </div>
112
+ );
113
+ }
114
+
115
+ return (
116
+ <button
117
+ key={providerType}
118
+ type="button"
119
+ onClick={() => onSelectProvider?.(detected!.id)}
120
+ aria-pressed={isSelected}
121
+ className={`flex min-h-16 items-center gap-2 rounded-lg border p-2.5 text-left transition-colors ${
122
+ isSelected
123
+ ? 'border-primary bg-primary/5'
124
+ : 'border-border bg-muted/35 hover:border-muted-foreground/30 hover:bg-muted/60'
125
+ }`}
126
+ >
127
+ {inner}
128
+ </button>
129
+ );
130
+ })}
131
+ </div>
132
+ </div>
133
+ )}
134
+
135
+ <div className="grid gap-2 sm:grid-cols-2">
136
+ <div className="flex items-start gap-3 p-3 rounded-lg border border-transparent bg-muted/50">
137
+ <SparklesIcon className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" />
138
+ <div className="flex-1">
139
+ <div className="text-sm font-medium">Ask from comments</div>
140
+ <div className="text-xs text-muted-foreground">
141
+ Use Ask AI from a comment popover to chat about selected text.
142
+ </div>
143
+ </div>
144
+ </div>
145
+ <div className="flex items-start gap-3 p-3 rounded-lg border border-transparent bg-muted/50">
146
+ <SparklesIcon className="w-4 h-4 mt-0.5 text-primary flex-shrink-0" />
147
+ <div className="flex-1">
148
+ <div className="text-sm font-medium">Open the side chat</div>
149
+ <div className="text-xs text-muted-foreground">
150
+ Use the AI button in the header for broader document questions.
151
+ </div>
152
+ </div>
153
+ </div>
154
+ </div>
155
+
156
+ {unsupportedLabels && (
157
+ <p className="text-[11px] leading-relaxed text-muted-foreground/70">
158
+ Not supported for Ask AI yet: {unsupportedLabels}.
159
+ </p>
160
+ )}
161
+ </div>
162
+
163
+ {/* Footer */}
164
+ <div className="p-4 border-t border-border flex justify-between items-center gap-3">
165
+ <p className="text-xs text-muted-foreground">
166
+ This notice only appears once.
167
+ </p>
168
+ <div className="flex items-center gap-2">
169
+ <button
170
+ onClick={onDismiss}
171
+ className="px-4 py-2 border border-border rounded-lg text-sm font-medium text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
172
+ >
173
+ Dismiss
174
+ </button>
175
+ <button
176
+ onClick={onOpenAI}
177
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"
178
+ >
179
+ Open AI Chat
180
+ </button>
181
+ </div>
182
+ </div>
183
+ </div>
184
+ </div>,
185
+ document.body
186
+ );
187
+ };
@@ -0,0 +1,299 @@
1
+ import React from 'react';
2
+ import {
3
+ ActionMenu,
4
+ ActionMenuDivider,
5
+ ActionMenuItem,
6
+ ActionMenuSectionLabel,
7
+ } from './ActionMenu';
8
+ import { useTheme } from './ThemeProvider';
9
+ import { SunIcon, MoonIcon, SystemIcon } from './icons/themeIcons';
10
+ import { ReviewAgentsIcon } from './ReviewAgentsIcon';
11
+ import { MenuVersionSection } from './MenuVersionSection';
12
+ import { TextShimmer } from './TextShimmer';
13
+ import type { UpdateInfo } from '../hooks/useUpdateCheck';
14
+ import type { Origin } from '@plannotator/core/agents';
15
+
16
+ interface PlanHeaderMenuProps {
17
+ appVersion: string;
18
+ updateInfo?: UpdateInfo | null;
19
+ origin?: Origin | null;
20
+ isWSL?: boolean;
21
+ onOpenSettings: () => void;
22
+ onOpenExport: () => void;
23
+ onCopyAgentInstructions: () => void;
24
+ onDownloadAnnotations: () => void;
25
+ onPrint: () => void;
26
+ onCopyShareLink: () => void;
27
+ onOpenImport: () => void;
28
+ onSaveToObsidian: () => void;
29
+ onSaveToBear: () => void;
30
+ onSaveToOctarine: () => void;
31
+ sharingEnabled: boolean;
32
+ isApiMode: boolean;
33
+ agentInstructionsEnabled: boolean;
34
+ obsidianConfigured: boolean;
35
+ bearConfigured: boolean;
36
+ octarineConfigured: boolean;
37
+ }
38
+
39
+ export const PlanHeaderMenu: React.FC<PlanHeaderMenuProps> = ({
40
+ appVersion,
41
+ updateInfo,
42
+ origin,
43
+ isWSL = false,
44
+ onOpenSettings,
45
+ onOpenExport,
46
+ onCopyAgentInstructions,
47
+ onDownloadAnnotations,
48
+ onPrint,
49
+ onCopyShareLink,
50
+ onOpenImport,
51
+ onSaveToObsidian,
52
+ onSaveToBear,
53
+ onSaveToOctarine,
54
+ sharingEnabled,
55
+ isApiMode,
56
+ agentInstructionsEnabled,
57
+ obsidianConfigured,
58
+ bearConfigured,
59
+ octarineConfigured,
60
+ }) => {
61
+ const { theme, setTheme } = useTheme();
62
+
63
+ const showUpdateDot = !!updateInfo?.updateAvailable && !updateInfo.dismissed;
64
+
65
+ const anyNotesAppConfigured =
66
+ isApiMode && (obsidianConfigured || bearConfigured || octarineConfigured);
67
+
68
+ return (
69
+ <ActionMenu
70
+ renderTrigger={({ isOpen, toggleMenu }) => (
71
+ <button
72
+ onClick={() => {
73
+ if (!isOpen && showUpdateDot) updateInfo?.dismiss();
74
+ toggleMenu();
75
+ }}
76
+ className={`relative flex items-center gap-1.5 p-1.5 md:px-2.5 md:py-1 rounded-md text-xs font-medium transition-colors ${
77
+ isOpen
78
+ ? 'bg-muted text-foreground'
79
+ : 'text-muted-foreground hover:text-foreground hover:bg-muted'
80
+ }`}
81
+ title="Options"
82
+ aria-label="Options"
83
+ aria-expanded={isOpen}
84
+ >
85
+ {isOpen ? <CloseIcon /> : <MenuIcon />}
86
+ {showUpdateDot ? (
87
+ <TextShimmer className="hidden md:inline text-xs font-medium" duration={2.5} spread={1.5}>
88
+ Options
89
+ </TextShimmer>
90
+ ) : (
91
+ <span className="hidden md:inline">Options</span>
92
+ )}
93
+ {showUpdateDot && (
94
+ <span className="absolute top-0.5 right-0.5 md:-top-0.5 md:-right-0.5 w-2 h-2 rounded-full bg-primary ring-2 ring-background" />
95
+ )}
96
+ </button>
97
+ )}
98
+ >
99
+ {({ closeMenu }) => (
100
+ <>
101
+ <div className="px-3 py-2 space-y-1.5">
102
+ <ActionMenuSectionLabel>Theme</ActionMenuSectionLabel>
103
+ <div className="flex items-center gap-1 rounded-lg bg-muted/50 p-0.5">
104
+ {(['light', 'dark', 'system'] as const).map((mode) => (
105
+ <button
106
+ key={mode}
107
+ onClick={() => {
108
+ closeMenu();
109
+ setTheme(mode);
110
+ }}
111
+ className={`flex flex-1 items-center justify-center gap-1.5 rounded-md px-2 py-1 text-[11px] font-medium transition-colors ${
112
+ theme === mode
113
+ ? 'bg-background text-foreground shadow-sm'
114
+ : 'text-muted-foreground hover:text-foreground'
115
+ }`}
116
+ >
117
+ {mode === 'light' ? <SunIcon /> : mode === 'dark' ? <MoonIcon /> : <SystemIcon />}
118
+ <span className="capitalize">{mode}</span>
119
+ </button>
120
+ ))}
121
+ </div>
122
+ </div>
123
+
124
+ <ActionMenuDivider />
125
+
126
+ <ActionMenuItem
127
+ onClick={() => {
128
+ closeMenu();
129
+ onOpenSettings();
130
+ }}
131
+ icon={<SettingsIcon />}
132
+ label="Settings"
133
+ />
134
+ <ActionMenuItem
135
+ onClick={() => {
136
+ closeMenu();
137
+ onOpenExport();
138
+ }}
139
+ icon={<ExportIcon />}
140
+ label="Export"
141
+ />
142
+ {agentInstructionsEnabled && (
143
+ <ActionMenuItem
144
+ onClick={() => {
145
+ closeMenu();
146
+ onCopyAgentInstructions();
147
+ }}
148
+ icon={<ReviewAgentsIcon />}
149
+ label="Agent Instructions"
150
+ subtitle="Copy agent instructions for external annotations"
151
+ />
152
+ )}
153
+
154
+ <ActionMenuDivider />
155
+
156
+ <ActionMenuItem
157
+ onClick={() => {
158
+ closeMenu();
159
+ onDownloadAnnotations();
160
+ }}
161
+ icon={<DownloadIcon />}
162
+ label="Download Annotations"
163
+ />
164
+ <ActionMenuItem
165
+ onClick={() => {
166
+ closeMenu();
167
+ onPrint();
168
+ }}
169
+ icon={<PrintIcon />}
170
+ label="Print / Save as PDF"
171
+ subtitle="Choose 'Save as PDF' in the print dialog"
172
+ />
173
+ {sharingEnabled && (
174
+ <ActionMenuItem
175
+ onClick={() => {
176
+ closeMenu();
177
+ onCopyShareLink();
178
+ }}
179
+ icon={<LinkIcon />}
180
+ label="Copy Share Link"
181
+ />
182
+ )}
183
+ {sharingEnabled && (
184
+ <ActionMenuItem
185
+ onClick={() => {
186
+ closeMenu();
187
+ onOpenImport();
188
+ }}
189
+ icon={<ImportIcon />}
190
+ label="Import Review"
191
+ />
192
+ )}
193
+
194
+ {anyNotesAppConfigured && (
195
+ <>
196
+ <ActionMenuDivider />
197
+ {obsidianConfigured && (
198
+ <ActionMenuItem
199
+ onClick={() => {
200
+ closeMenu();
201
+ onSaveToObsidian();
202
+ }}
203
+ icon={<NoteIcon />}
204
+ label="Save to Obsidian"
205
+ />
206
+ )}
207
+ {bearConfigured && (
208
+ <ActionMenuItem
209
+ onClick={() => {
210
+ closeMenu();
211
+ onSaveToBear();
212
+ }}
213
+ icon={<NoteIcon />}
214
+ label="Save to Bear"
215
+ />
216
+ )}
217
+ {octarineConfigured && (
218
+ <ActionMenuItem
219
+ onClick={() => {
220
+ closeMenu();
221
+ onSaveToOctarine();
222
+ }}
223
+ icon={<NoteIcon />}
224
+ label="Save to Octarine"
225
+ />
226
+ )}
227
+ </>
228
+ )}
229
+
230
+ <ActionMenuDivider />
231
+
232
+ <MenuVersionSection
233
+ appVersion={appVersion}
234
+ updateInfo={updateInfo}
235
+ origin={origin}
236
+ isWSL={isWSL}
237
+ closeMenu={closeMenu}
238
+ />
239
+ </>
240
+ )}
241
+ </ActionMenu>
242
+ );
243
+ };
244
+
245
+ const MenuIcon = () => (
246
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
247
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4 6h16M4 12h16M4 18h16" />
248
+ </svg>
249
+ );
250
+
251
+ const CloseIcon = () => (
252
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2.5}>
253
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
254
+ </svg>
255
+ );
256
+
257
+ const SettingsIcon = () => (
258
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
259
+ <path strokeLinecap="round" strokeLinejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
260
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
261
+ </svg>
262
+ );
263
+
264
+ const ExportIcon = () => (
265
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
266
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
267
+ </svg>
268
+ );
269
+
270
+ const DownloadIcon = () => (
271
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
272
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
273
+ </svg>
274
+ );
275
+
276
+ const PrintIcon = () => (
277
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
278
+ <path strokeLinecap="round" strokeLinejoin="round" d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
279
+ </svg>
280
+ );
281
+
282
+ const LinkIcon = () => (
283
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
284
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
285
+ </svg>
286
+ );
287
+
288
+ const ImportIcon = () => (
289
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
290
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15 3h4a2 2 0 012 2v14a2 2 0 01-2 2h-4M10 17l5-5-5-5M15 12H3" />
291
+ </svg>
292
+ );
293
+
294
+ const NoteIcon = () => (
295
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
296
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
297
+ </svg>
298
+ );
299
+
@@ -0,0 +1,91 @@
1
+ import React, { useCallback } from 'react';
2
+ import * as Dialog from '@radix-ui/react-dialog';
3
+
4
+ const ANNOTATION_SELECTORS = [
5
+ '.annotation-toolbar',
6
+ '[data-comment-popover="true"]',
7
+ '[data-floating-picker="true"]',
8
+ ];
9
+
10
+ interface PopoutDialogProps {
11
+ open: boolean;
12
+ onClose: () => void;
13
+ title: string;
14
+ container?: HTMLElement | null;
15
+ className?: string;
16
+ children: React.ReactNode;
17
+ /** Extra data attributes to spread onto Dialog.Content */
18
+ dataAttributes?: Record<string, string>;
19
+ }
20
+
21
+ export const PopoutDialog: React.FC<PopoutDialogProps> = ({
22
+ open,
23
+ onClose,
24
+ title,
25
+ container,
26
+ className,
27
+ children,
28
+ dataAttributes,
29
+ }) => {
30
+ const handleBackdropClick = useCallback(
31
+ (e: React.MouseEvent) => {
32
+ const target = e.target as Element;
33
+ if (ANNOTATION_SELECTORS.some((sel) => target.closest(sel))) return;
34
+ onClose();
35
+ },
36
+ [onClose],
37
+ );
38
+
39
+ const handleInteractOutside = useCallback(
40
+ (e: Event) => {
41
+ const target = e.target as Element | null;
42
+ if (!target) return;
43
+ if (ANNOTATION_SELECTORS.some((sel) => target.closest(sel))) {
44
+ e.preventDefault();
45
+ }
46
+ },
47
+ [],
48
+ );
49
+
50
+ return (
51
+ <Dialog.Root
52
+ open={open}
53
+ onOpenChange={(next) => { if (!next) onClose(); }}
54
+ modal={false}
55
+ >
56
+ <Dialog.Portal container={container ?? undefined}>
57
+ {/* Radix ignores Dialog.Overlay when modal={false}, so we use a plain
58
+ div for the backdrop. This gives us the dark scrim + blur while
59
+ keeping annotation toolbars (which portal outside the dialog)
60
+ interactive. */}
61
+ <div
62
+ className="fixed inset-0 z-50 bg-black/50 backdrop-blur-[2px]"
63
+ onClick={handleBackdropClick}
64
+ aria-hidden="true"
65
+ />
66
+ <Dialog.Content
67
+ className={`fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 bg-background border border-border rounded-xl shadow-2xl flex flex-col overflow-hidden ${className ?? 'w-[calc(100vw-4rem)] max-w-[min(calc(100vw-4rem),1500px)] max-h-[calc(100vh-4rem)]'}`}
68
+ data-popout="true"
69
+ aria-describedby={undefined}
70
+ onOpenAutoFocus={(e) => e.preventDefault()}
71
+ onInteractOutside={handleInteractOutside}
72
+ {...dataAttributes}
73
+ >
74
+ <Dialog.Title className="sr-only">{title}</Dialog.Title>
75
+ <Dialog.Close asChild>
76
+ <button
77
+ className="absolute top-3 right-3 z-20 p-1.5 rounded-md text-muted-foreground/70 hover:bg-muted hover:text-foreground transition-colors"
78
+ aria-label="Close"
79
+ >
80
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
81
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
82
+ </svg>
83
+ </button>
84
+ </Dialog.Close>
85
+
86
+ {children}
87
+ </Dialog.Content>
88
+ </Dialog.Portal>
89
+ </Dialog.Root>
90
+ );
91
+ };
@@ -0,0 +1,27 @@
1
+ import * as PopoverPrimitive from '@radix-ui/react-popover';
2
+ import React from 'react';
3
+
4
+ const Popover = PopoverPrimitive.Root;
5
+ const PopoverTrigger = PopoverPrimitive.Trigger;
6
+ const PopoverAnchor = PopoverPrimitive.Anchor;
7
+
8
+ const PopoverContent = React.forwardRef<
9
+ React.ComponentRef<typeof PopoverPrimitive.Content>,
10
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
11
+ >(({ className, align = 'center', sideOffset = 6, children, ...props }, ref) => (
12
+ <PopoverPrimitive.Portal>
13
+ <PopoverPrimitive.Content
14
+ ref={ref}
15
+ align={align}
16
+ sideOffset={sideOffset}
17
+ className={`bg-popover text-popover-foreground border border-border/50 rounded-lg shadow-xl z-[100] outline-none popover-enter ${className ?? ''}`}
18
+ {...props}
19
+ >
20
+ {children}
21
+ </PopoverPrimitive.Content>
22
+ </PopoverPrimitive.Portal>
23
+ ));
24
+
25
+ PopoverContent.displayName = 'PopoverContent';
26
+
27
+ export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
@@ -0,0 +1,51 @@
1
+ /// <reference path="../globals.d.ts" />
2
+ import type React from 'react';
3
+ import codexPng from '../assets/icon-codex.png';
4
+
5
+ /** Claude icon — extracted from apps/marketing/public/assets/icon-claude.svg */
6
+ export const ClaudeIcon: React.FC<{ className?: string }> = ({ className = 'w-4 h-4' }) => (
7
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className={className}>
8
+ <path d="m6.283 21.28 6.293-3.531.106-.306-.106-.171h-.307l-1.051-.065-3.596-.097-3.118-.13-3.021-.162-.761-.161-.712-.94.073-.469.639-.429.916.08 2.023.138 3.037.209 2.203.13 3.263.339h.518l.073-.21-.177-.129-.138-.13-3.142-2.129-3.401-2.25-1.782-1.296-.963-.656-.486-.616-.21-1.343.875-.963 1.175.08.3.08 1.19.915 2.542 1.967 3.319 2.445.486.404.194-.138.024-.097-.218-.365-1.806-3.263-1.926-3.32-.857-1.375-.227-.825c-.08-.339-.138-.624-.138-.972L8.384.177 8.935 0l1.328.177.56.486.824 1.887 1.337 2.972 2.073 4.04.607 1.199.324 1.11.121.339h.21v-.194l.17-2.276.315-2.795.307-3.596.106-1.012.501-1.214.995-.657.778.372.639.916-.088.591-.381 2.471-.745 3.87-.485 2.591h.282l.324-.324 1.311-1.74 2.203-2.754.972-1.093 1.133-1.207.728-.574h1.376l1.013 1.505-.454 1.555-1.416 1.797-1.175 1.522-1.685 2.268-1.051 1.814.097.144.25-.023 3.805-.81 2.056-.372 2.454-.421 1.11.518.12.527-.436 1.078-2.624.648-3.077.615-4.582 1.084-.057.041.065.08 2.065.195.883.047h2.162l4.025.3 1.052.696.63.851-.106.647-1.619.825-2.186-.518-5.1-1.214-1.75-.436h-.242v.145l1.458 1.425 2.671 2.412 3.346 3.11.17.769-.43.607-.453-.065-2.939-2.211-1.134-.996-2.568-2.162h-.17v.227l.591.866 3.125 4.697.162 1.441-.226.468-.81.283-.89-.162-1.829-2.568-1.888-2.891-1.522-2.592-.186.106-.898 9.677-.421.495-.972.371-.81-.615-.43-.996.43-1.967.518-2.568.422-2.041.38-2.535.226-.842-.015-.056-.185.023-1.912 2.624-2.906 3.928-2.3 2.462-.551.218-.954-.494.088-.883.533-.787 3.184-4.049 1.919-2.509 1.24-1.449-.009-.21h-.073l-8.455 5.49-1.505.194-.648-.607.08-.995.307-.324 2.542-1.749-.009.008z" fill="#d97757" />
9
+ </svg>
10
+ );
11
+
12
+ /** Codex icon — PNG from apps/marketing/public/assets/icon-codex.png */
13
+ export const CodexIcon: React.FC<{ className?: string }> = ({ className = 'w-4 h-4' }) => (
14
+ <img src={codexPng} alt="" className={`${className} rounded-sm`} />
15
+ );
16
+
17
+ /** Pi icon — extracted from apps/marketing/public/assets/icon-pi.svg */
18
+ export const PiIcon: React.FC<{ className?: string }> = ({ className = 'w-4 h-4' }) => (
19
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 800 800" className={className}>
20
+ <path fill="currentColor" fillRule="evenodd" d="M165.29 165.29H517.36V400H400V517.36H282.65V634.72H165.29ZM282.65 282.65V400H400V282.65Z"/>
21
+ <path fill="currentColor" d="M517.36 400H634.72V634.72H517.36Z"/>
22
+ </svg>
23
+ );
24
+
25
+ /** OpenCode icon — extracted from apps/marketing/public/assets/icon-opencode-dark.svg */
26
+ export const OpenCodeIcon: React.FC<{ className?: string }> = ({ className = 'w-4 h-4' }) => (
27
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" className={className}>
28
+ <path d="M3 32V0h26v32zM22 7H10v18h12z" fill="currentColor"/>
29
+ <path d="M10 13h12v12H10z" fill="currentColor" opacity={0.4}/>
30
+ </svg>
31
+ );
32
+
33
+ /** Generic fallback icon for unknown providers */
34
+ const GenericProviderIcon: React.FC<{ className?: string }> = ({ className = 'w-4 h-4' }) => (
35
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth={2} className={className}>
36
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9.75 3.104v5.714a2.25 2.25 0 01-.659 1.591L5 14.5M9.75 3.104c-.251.023-.501.05-.75.082m.75-.082a24.301 24.301 0 014.5 0m0 0v5.714a2.25 2.25 0 00.659 1.591L19 14.5m-4.75-11.396c.251.023.501.05.75.082M12 21a8.966 8.966 0 005.982-2.275M12 21a8.966 8.966 0 01-5.982-2.275M12 21V14.5" />
37
+ </svg>
38
+ );
39
+
40
+ /** Provider metadata: maps provider type name to display label and icon component. */
41
+ export const PROVIDER_META: Record<string, { label: string; icon: React.FC<{ className?: string }> }> = {
42
+ 'claude-agent-sdk': { label: 'Claude', icon: ClaudeIcon },
43
+ 'codex-sdk': { label: 'Codex', icon: CodexIcon },
44
+ 'pi-sdk': { label: 'Pi', icon: PiIcon },
45
+ 'opencode-sdk': { label: 'OpenCode', icon: OpenCodeIcon },
46
+ };
47
+
48
+ /** Get provider metadata, with fallback for unknown providers. */
49
+ export function getProviderMeta(providerName: string): { label: string; icon: React.FC<{ className?: string }> } {
50
+ return PROVIDER_META[providerName] ?? { label: providerName, icon: GenericProviderIcon };
51
+ }
@@ -0,0 +1,12 @@
1
+ import React from "react";
2
+
3
+ export const PullRequestIcon: React.FC<{ className?: string }> = ({ className }) => (
4
+ <svg
5
+ className={className}
6
+ viewBox="0 0 768 1024"
7
+ fill="currentColor"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ >
10
+ <path d="M704 722s0-306 0-402-96-192-192-192c-64 0-64 0-64 0v-128l-192 192 192 192v-128s32 0 64 0 64 32 64 64 0 402 0 402c-38 22-64 63-64 110 0 71 57 128 128 128s128-57 128-128c0-47-26-88-64-110z m-64 174c-35 0-64-29-64-64s29-64 64-64 64 29 64 64-29 64-64 64z m-512-832c-71 0-128 57-128 128 0 47 26 88 64 110v419c-38 22-64 63-64 110 0 71 57 128 128 128s128-57 128-128c0-47-26-88-64-110v-419c38-22 64-63 64-110 0-71-57-128-128-128z m0 832c-35 0-64-29-64-64s29-64 64-64 64 29 64 64-29 64-64 64z m0-640c-35 0-64-29-64-64s29-64 64-64 64 29 64 64-29 64-64 64z" />
11
+ </svg>
12
+ );