@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
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
+ }
@@ -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
+ }