@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,95 @@
1
+ import React from 'react';
2
+ import { getProviderMeta } from '../ProviderIcons';
3
+ import { type AIProviderOption } from '../../utils/aiProvider';
4
+
5
+ interface AIProviderBarProps {
6
+ providers: AIProviderOption[];
7
+ selectedProviderId: string | null;
8
+ selectedModel: string | null;
9
+ selectedReasoningEffort?: string | null;
10
+ onProviderChange: (providerId: string) => void;
11
+ onModelChange: (model: string) => void;
12
+ onReasoningEffortChange?: (effort: string | null) => void;
13
+ }
14
+
15
+ export const AIProviderBar: React.FC<AIProviderBarProps> = ({
16
+ providers,
17
+ selectedProviderId,
18
+ selectedModel,
19
+ selectedReasoningEffort,
20
+ onProviderChange,
21
+ onModelChange,
22
+ onReasoningEffortChange,
23
+ }) => {
24
+ if (providers.length === 0) {
25
+ return (
26
+ <div className="border-t border-border/50 px-2 py-1.5 text-[11px] text-muted-foreground/50">
27
+ No AI providers available
28
+ </div>
29
+ );
30
+ }
31
+
32
+ const currentProvider = providers.find(p => p.id === selectedProviderId) ?? providers[0];
33
+ const effectiveProviderId = currentProvider?.id ?? '';
34
+ const models = currentProvider?.models ?? [];
35
+ const defaultModel = models.find(m => m.default) ?? models[0];
36
+ const effectiveModel = selectedModel ?? defaultModel?.id ?? '';
37
+ const currentModel = models.find(m => m.id === effectiveModel) ?? defaultModel;
38
+ const reasoningEfforts = currentModel?.reasoningEfforts ?? [];
39
+ const meta = getProviderMeta(currentProvider?.name ?? 'AI');
40
+ const Icon = meta.icon;
41
+ // Show only when the selected model actually reports reasoning efforts.
42
+ const showReasoningEffort = !!onReasoningEffortChange && reasoningEfforts.length > 0;
43
+
44
+ return (
45
+ <div className="border-t border-border/50 px-2 py-1.5 flex items-center gap-1.5 text-[11px] text-muted-foreground">
46
+ <Icon className="w-3.5 h-3.5 flex-shrink-0" />
47
+ <select
48
+ value={effectiveProviderId ?? ''}
49
+ onChange={(event) => onProviderChange(event.target.value)}
50
+ className="min-w-0 max-w-[8rem] bg-transparent text-[11px] text-foreground focus:outline-none"
51
+ aria-label="AI provider"
52
+ >
53
+ {providers.map(provider => {
54
+ const providerMeta = getProviderMeta(provider.name);
55
+ return (
56
+ <option key={provider.id} value={provider.id}>
57
+ {providerMeta.label}
58
+ </option>
59
+ );
60
+ })}
61
+ </select>
62
+
63
+ {models.length > 0 && (
64
+ <select
65
+ value={effectiveModel}
66
+ onChange={(event) => onModelChange(event.target.value)}
67
+ className="min-w-0 flex-1 bg-transparent text-[11px] text-foreground focus:outline-none"
68
+ aria-label="AI model"
69
+ >
70
+ {models.map(model => (
71
+ <option key={model.id} value={model.id}>
72
+ {model.label}
73
+ </option>
74
+ ))}
75
+ </select>
76
+ )}
77
+
78
+ {showReasoningEffort && (
79
+ <select
80
+ value={selectedReasoningEffort ?? ''}
81
+ onChange={(event) => onReasoningEffortChange?.(event.target.value || null)}
82
+ className="w-16 bg-transparent text-[11px] text-foreground focus:outline-none"
83
+ aria-label="Reasoning effort"
84
+ >
85
+ <option value="">Auto</option>
86
+ {reasoningEfforts.map(effort => (
87
+ <option key={effort.id} value={effort.id}>
88
+ {effort.label}
89
+ </option>
90
+ ))}
91
+ </select>
92
+ )}
93
+ </div>
94
+ );
95
+ };
@@ -0,0 +1,313 @@
1
+ import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { AIChatEntry, PendingPermission } from '../../hooks/useAIChat';
3
+ import type { AIProviderOption } from '../../utils/aiProvider';
4
+ import { formatRelativeTime, renderChatMarkdown } from '../../utils/aiChatFormat';
5
+ import { OverlayScrollArea } from '../OverlayScrollArea';
6
+ import { SparklesIcon } from '../SparklesIcon';
7
+ import { AIProviderBar } from './AIProviderBar';
8
+ import { submitHint } from '../../utils/platform';
9
+
10
+ interface DocumentAIChatPanelProps {
11
+ messages: AIChatEntry[];
12
+ isCreatingSession: boolean;
13
+ isStreaming: boolean;
14
+ onAskGeneral?: (question: string) => void;
15
+ /** Stop the in-flight answer. When provided, the input shows a Stop button while streaming. */
16
+ onStop?: () => void;
17
+ permissionRequests?: PendingPermission[];
18
+ onRespondToPermission?: (requestId: string, allow: boolean) => void;
19
+ aiProviders?: AIProviderOption[];
20
+ aiConfig?: { providerId: string | null; model: string | null; reasoningEffort?: string | null };
21
+ onAIConfigChange?: (config: { providerId?: string | null; model?: string | null; reasoningEffort?: string | null }) => void;
22
+ }
23
+
24
+ function truncate(text: string, max = 180): string {
25
+ if (text.length <= max) return text;
26
+ return `${text.slice(0, max).trimEnd()}...`;
27
+ }
28
+
29
+ function formatToolInput(toolName: string, input: Record<string, unknown>): string | null {
30
+ if (!input || Object.keys(input).length === 0) return null;
31
+
32
+ if (toolName === 'Bash' && typeof input.command === 'string') {
33
+ return input.command;
34
+ }
35
+ if ((toolName === 'Read' || toolName === 'Write' || toolName === 'Edit') && typeof input.file_path === 'string') {
36
+ return input.file_path;
37
+ }
38
+ if (toolName === 'Glob' && typeof input.pattern === 'string') {
39
+ return input.pattern;
40
+ }
41
+ if (toolName === 'Grep' && typeof input.pattern === 'string') {
42
+ const path = typeof input.path === 'string' ? ` in ${input.path}` : '';
43
+ return `${input.pattern}${path}`;
44
+ }
45
+ if ((toolName === 'WebFetch' || toolName === 'WebSearch') && typeof input.url === 'string') {
46
+ return input.url;
47
+ }
48
+
49
+ try {
50
+ return truncate(JSON.stringify(input), 240);
51
+ } catch {
52
+ return String(input);
53
+ }
54
+ }
55
+
56
+ export const DocumentAIChatPanel: React.FC<DocumentAIChatPanelProps> = ({
57
+ messages,
58
+ isCreatingSession,
59
+ isStreaming,
60
+ onAskGeneral,
61
+ onStop,
62
+ permissionRequests = [],
63
+ onRespondToPermission,
64
+ aiProviders = [],
65
+ aiConfig,
66
+ onAIConfigChange,
67
+ }) => {
68
+ const scrollRef = useRef<HTMLDivElement>(null);
69
+ const [generalInput, setGeneralInput] = useState('');
70
+ const latestMessage = messages[messages.length - 1];
71
+ const latestResponseText = latestMessage?.response.text ?? '';
72
+
73
+ useEffect(() => {
74
+ if (!scrollRef.current) return;
75
+ const last = scrollRef.current.querySelector('[data-ai-message]:last-child');
76
+ last?.scrollIntoView({ behavior: 'smooth', block: 'end' });
77
+ }, [messages.length, latestResponseText]);
78
+
79
+ const handleGeneralSubmit = useCallback(() => {
80
+ const question = generalInput.trim();
81
+ if (!question || !onAskGeneral) return;
82
+ onAskGeneral(question);
83
+ setGeneralInput('');
84
+ }, [generalInput, onAskGeneral]);
85
+
86
+ return (
87
+ <div className="flex flex-col h-full">
88
+ <OverlayScrollArea className="flex-1 min-h-0">
89
+ <div ref={scrollRef} className="p-2 space-y-3">
90
+ {messages.length === 0 && !isCreatingSession && (
91
+ <div className="flex flex-col items-center justify-center h-48 text-center px-4 text-muted-foreground">
92
+ <div className="w-10 h-10 rounded-full bg-muted/50 flex items-center justify-center mb-3">
93
+ <SparklesIcon className="w-5 h-5" />
94
+ </div>
95
+ <p className="text-xs">
96
+ {onAskGeneral ? (
97
+ <>Select text and click <strong>Ask AI</strong>, or ask a general question below.</>
98
+ ) : (
99
+ <>Select text and click <strong>Ask AI</strong>.</>
100
+ )}
101
+ </p>
102
+ </div>
103
+ )}
104
+
105
+ {isCreatingSession && messages.length === 0 && (
106
+ <div className="text-xs text-muted-foreground text-center py-4">
107
+ <span className="ai-streaming-cursor" /> Starting AI session...
108
+ </div>
109
+ )}
110
+
111
+ {messages.map(entry => (
112
+ <DocumentQAPair key={entry.question.id} entry={entry} />
113
+ ))}
114
+ </div>
115
+ </OverlayScrollArea>
116
+
117
+ {/* Pending permission requests — pinned just above the input/model bar so
118
+ the user sees them right where they act, not buried at the top. */}
119
+ {permissionRequests.filter(p => !p.decided).length > 0 && (
120
+ <div className="border-t border-border/50 p-2 space-y-2">
121
+ {permissionRequests.filter(p => !p.decided).map(permission => (
122
+ <PermissionCard
123
+ key={permission.requestId}
124
+ permission={permission}
125
+ onRespond={onRespondToPermission ?? (() => {})}
126
+ />
127
+ ))}
128
+ </div>
129
+ )}
130
+
131
+ <AIProviderBar
132
+ providers={aiProviders}
133
+ selectedProviderId={aiConfig?.providerId ?? null}
134
+ selectedModel={aiConfig?.model ?? null}
135
+ selectedReasoningEffort={aiConfig?.reasoningEffort ?? null}
136
+ onProviderChange={(providerId) => onAIConfigChange?.({ providerId })}
137
+ onModelChange={(model) => onAIConfigChange?.({ model })}
138
+ onReasoningEffortChange={(reasoningEffort) => onAIConfigChange?.({ reasoningEffort })}
139
+ />
140
+
141
+ {onAskGeneral && (
142
+ <GeneralInput
143
+ value={generalInput}
144
+ onChange={setGeneralInput}
145
+ onSubmit={handleGeneralSubmit}
146
+ disabled={isStreaming}
147
+ isStreaming={isStreaming}
148
+ onStop={onStop}
149
+ />
150
+ )}
151
+ </div>
152
+ );
153
+ };
154
+
155
+ const DocumentQAPair = memo<{ entry: AIChatEntry }>(({ entry }) => {
156
+ const { question, response } = entry;
157
+ const renderedResponse = useMemo(
158
+ () => response.text ? renderChatMarkdown(response.text) : null,
159
+ [response.text],
160
+ );
161
+ const scope = question.scope;
162
+
163
+ return (
164
+ <div data-ai-message data-question-id={question.id} className="flex flex-col gap-1.5">
165
+ <div className="p-2.5 rounded-lg border border-transparent hover:bg-muted/30 transition-colors">
166
+ <div className="flex items-center justify-between gap-2 mb-1">
167
+ <div className="flex items-center gap-1.5 min-w-0">
168
+ {scope?.kind === 'selection' && (
169
+ <span className="text-[9px] font-semibold uppercase tracking-wider px-1.5 py-0.5 rounded bg-primary/10 text-primary">
170
+ selection
171
+ </span>
172
+ )}
173
+ {scope?.label && (
174
+ <span className="text-[10px] text-muted-foreground truncate">
175
+ {scope.label}
176
+ </span>
177
+ )}
178
+ </div>
179
+ <span className="text-[10px] text-muted-foreground/50 flex-shrink-0">
180
+ {formatRelativeTime(question.createdAt)}
181
+ </span>
182
+ </div>
183
+ {scope?.text && (
184
+ <p className="text-[10px] text-muted-foreground/70 border-l border-border pl-2 mb-1.5 line-clamp-3">
185
+ {truncate(scope.text)}
186
+ </p>
187
+ )}
188
+ <p className="text-xs text-foreground/80 whitespace-pre-wrap">{question.prompt}</p>
189
+ </div>
190
+
191
+ <div className="group relative p-2.5 rounded-lg border border-border/50 bg-popover/50 hover:bg-muted/30 transition-colors">
192
+ {response.error ? (
193
+ <p className="text-xs text-destructive">{response.error}</p>
194
+ ) : response.text ? (
195
+ <div className="text-xs">
196
+ {renderedResponse}
197
+ {response.isStreaming && <span className="ai-streaming-cursor inline-block ml-0.5" />}
198
+ </div>
199
+ ) : response.isStreaming ? (
200
+ <span className="text-xs text-muted-foreground">
201
+ <span className="ai-streaming-cursor" /> Thinking...
202
+ </span>
203
+ ) : null}
204
+ </div>
205
+ </div>
206
+ );
207
+ });
208
+
209
+ const PermissionCard: React.FC<{
210
+ permission: PendingPermission;
211
+ onRespond: (requestId: string, allow: boolean) => void;
212
+ }> = ({ permission, onRespond }) => {
213
+ const label = permission.title || permission.displayName || permission.toolName;
214
+ const toolInput = formatToolInput(permission.toolName, permission.toolInput);
215
+
216
+ return (
217
+ <div className="p-2.5 rounded-lg border border-warning/30 bg-warning/5">
218
+ <p className="text-[10px] font-medium text-warning uppercase tracking-wider mb-1">
219
+ Permission Request
220
+ </p>
221
+ <p className="text-xs font-mono text-foreground/80 break-all">
222
+ {label}
223
+ </p>
224
+ {toolInput && (
225
+ <p className="mt-1 px-2 py-1 rounded bg-background/60 border border-warning/20 text-[10px] font-mono text-foreground/80 break-all">
226
+ {toolInput}
227
+ </p>
228
+ )}
229
+ {permission.description && (
230
+ <p className="text-[10px] text-muted-foreground mt-0.5">{permission.description}</p>
231
+ )}
232
+ <div className="flex gap-2 mt-2">
233
+ <button
234
+ onClick={() => onRespond(permission.requestId, true)}
235
+ className="flex-1 px-2 py-1.5 rounded-md bg-primary text-primary-foreground text-[10px] font-medium hover:opacity-90 transition-opacity"
236
+ >
237
+ Allow
238
+ </button>
239
+ <button
240
+ onClick={() => onRespond(permission.requestId, false)}
241
+ className="flex-1 px-2 py-1.5 rounded-md bg-muted text-foreground text-[10px] font-medium hover:bg-muted/80 transition-colors"
242
+ >
243
+ Deny
244
+ </button>
245
+ </div>
246
+ </div>
247
+ );
248
+ };
249
+
250
+ const GeneralInput: React.FC<{
251
+ value: string;
252
+ onChange: (value: string) => void;
253
+ onSubmit: () => void;
254
+ disabled?: boolean;
255
+ isStreaming?: boolean;
256
+ onStop?: () => void;
257
+ }> = ({ value, onChange, onSubmit, disabled, isStreaming, onStop }) => {
258
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
259
+
260
+ useEffect(() => {
261
+ const el = textareaRef.current;
262
+ if (!el) return;
263
+ el.style.height = 'auto';
264
+ el.style.height = `${Math.min(el.scrollHeight, 120)}px`;
265
+ }, [value]);
266
+
267
+ return (
268
+ <div className="border-t border-border/50 p-2">
269
+ <div className="flex items-end gap-1.5">
270
+ <textarea
271
+ ref={textareaRef}
272
+ value={value}
273
+ onChange={(event) => onChange(event.target.value)}
274
+ placeholder="Ask about this document..."
275
+ rows={1}
276
+ className="flex-1 px-2.5 py-1.5 bg-muted rounded-md text-xs text-foreground placeholder:text-muted-foreground/50 resize-none focus:outline-none focus:ring-1 focus:ring-primary/50 leading-relaxed"
277
+ style={{ maxHeight: 120 }}
278
+ disabled={disabled}
279
+ onKeyDown={(event) => {
280
+ if (event.key === 'Enter' && (event.metaKey || event.ctrlKey) && !event.nativeEvent.isComposing && !disabled) {
281
+ event.preventDefault();
282
+ onSubmit();
283
+ }
284
+ }}
285
+ />
286
+ {isStreaming && onStop ? (
287
+ <button
288
+ onClick={onStop}
289
+ className="p-1.5 mb-px rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors flex-shrink-0"
290
+ title="Stop generating"
291
+ aria-label="Stop generating"
292
+ >
293
+ <svg className="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
294
+ <rect x="6" y="6" width="12" height="12" rx="2" />
295
+ </svg>
296
+ </button>
297
+ ) : (
298
+ <button
299
+ onClick={onSubmit}
300
+ disabled={disabled || !value.trim()}
301
+ className="p-1.5 mb-px rounded-md text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-30 disabled:cursor-not-allowed transition-colors flex-shrink-0"
302
+ title={`Send (${submitHint})`}
303
+ aria-label="Send AI question"
304
+ >
305
+ <svg className="w-3.5 h-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
306
+ <path strokeLinecap="round" strokeLinejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
307
+ </svg>
308
+ </button>
309
+ )}
310
+ </div>
311
+ </div>
312
+ );
313
+ };
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import type { AlertKind } from '../../types';
3
+ import { renderProseBody } from './proseBody';
4
+
5
+ interface AlertBlockProps {
6
+ blockId: string;
7
+ kind: AlertKind;
8
+ body: string;
9
+ onOpenLinkedDoc?: (path: string) => void;
10
+ onOpenCodeFile?: (path: string) => void;
11
+ imageBaseDir?: string;
12
+ onImageClick?: (src: string, alt: string) => void;
13
+ githubRepo?: string;
14
+ onNavigateAnchor?: (hash: string) => void;
15
+ }
16
+
17
+ const TITLE: Record<AlertKind, string> = {
18
+ note: 'Note',
19
+ tip: 'Tip',
20
+ warning: 'Warning',
21
+ caution: 'Caution',
22
+ important: 'Important',
23
+ };
24
+
25
+ const Icon: React.FC<{ kind: AlertKind }> = ({ kind }) => {
26
+ const common = { viewBox: '0 0 16 16', width: '16', height: '16', fill: 'currentColor', 'aria-hidden': true as const };
27
+ switch (kind) {
28
+ case 'note':
29
+ return <svg {...common}><path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>;
30
+ case 'tip':
31
+ return <svg {...common}><path d="M8 1.5c-2.363 0-4 1.69-4 3.75 0 .984.424 1.625.984 2.304l.214.253c.223.264.47.556.673.848.284.411.537.896.621 1.49a.75.75 0 0 1-1.484.211c-.04-.282-.163-.547-.37-.847a8.456 8.456 0 0 0-.542-.68c-.084-.1-.173-.205-.268-.32C3.201 7.75 2.5 6.766 2.5 5.25 2.5 2.31 4.863 0 8 0s5.5 2.31 5.5 5.25c0 1.516-.701 2.5-1.328 3.259-.095.115-.184.22-.268.319-.207.245-.383.453-.541.681-.208.3-.33.565-.37.847a.751.751 0 0 1-1.485-.212c.084-.593.337-1.078.621-1.489.203-.292.45-.584.673-.848.075-.088.147-.173.213-.253.561-.679.985-1.32.985-2.304 0-2.06-1.637-3.75-4-3.75ZM5.75 12h4.5a.75.75 0 0 1 0 1.5h-4.5a.75.75 0 0 1 0-1.5ZM6 15.25a.75.75 0 0 1 .75-.75h2.5a.75.75 0 0 1 0 1.5h-2.5a.75.75 0 0 1-.75-.75Z"/></svg>;
32
+ case 'warning':
33
+ return <svg {...common}><path d="M6.457 1.047c.659-1.234 2.427-1.234 3.086 0l6.082 11.378A1.75 1.75 0 0 1 14.082 15H1.918a1.75 1.75 0 0 1-1.543-2.575Zm1.763.707a.25.25 0 0 0-.44 0L1.698 13.132a.25.25 0 0 0 .22.368h12.164a.25.25 0 0 0 .22-.368Zm.53 3.996v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 11a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg>;
34
+ case 'caution':
35
+ return <svg {...common}><path d="M4.47.22A.749.749 0 0 1 5 0h6c.199 0 .389.079.53.22l4.25 4.25c.141.14.22.331.22.53v6a.749.749 0 0 1-.22.53l-4.25 4.25A.749.749 0 0 1 11 16H5a.749.749 0 0 1-.53-.22L.22 11.53A.749.749 0 0 1 0 11V5c0-.199.079-.389.22-.53Zm.84 1.28L1.5 5.31v5.38l3.81 3.81h5.38l3.81-3.81V5.31L10.69 1.5ZM8 4a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0v-3.5A.75.75 0 0 1 8 4Zm0 8a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/></svg>;
36
+ case 'important':
37
+ return <svg {...common}><path d="M0 1.75C0 .784.784 0 1.75 0h12.5C15.216 0 16 .784 16 1.75v9.5A1.75 1.75 0 0 1 14.25 13H8.06l-2.573 2.573A1.458 1.458 0 0 1 3 14.543V13H1.75A1.75 1.75 0 0 1 0 11.25Zm1.75-.25a.25.25 0 0 0-.25.25v9.5c0 .138.112.25.25.25h2a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h6.5a.25.25 0 0 0 .25-.25v-9.5a.25.25 0 0 0-.25-.25Zm7 2.25v2.5a.75.75 0 0 1-1.5 0v-2.5a.75.75 0 0 1 1.5 0ZM9 9a1 1 0 1 1-2 0 1 1 0 0 1 2 0Z"/></svg>;
38
+ }
39
+ };
40
+
41
+ export const AlertBlock: React.FC<AlertBlockProps> = ({
42
+ blockId, kind, body, onOpenLinkedDoc, onOpenCodeFile, imageBaseDir, onImageClick, githubRepo, onNavigateAnchor,
43
+ }) => {
44
+ return (
45
+ <div
46
+ className={`alert alert-${kind} my-4 pl-4 pr-3 py-2 border-l-[3px]`}
47
+ data-block-id={blockId}
48
+ data-block-type="alert"
49
+ data-alert-kind={kind}
50
+ >
51
+ <div className="alert-title flex items-center gap-2 font-semibold mb-1">
52
+ <Icon kind={kind} />
53
+ <span>{TITLE[kind]}</span>
54
+ </div>
55
+ {renderProseBody({ body, imageBaseDir, onImageClick, onOpenLinkedDoc, onOpenCodeFile, onNavigateAnchor, githubRepo })}
56
+ </div>
57
+ );
58
+ };
@@ -0,0 +1,60 @@
1
+ import React from 'react';
2
+ import { renderProseBody } from './proseBody';
3
+
4
+ interface CalloutProps {
5
+ blockId: string;
6
+ kind: string;
7
+ body: string;
8
+ containerClassName: string;
9
+ blockType: 'alert' | 'directive';
10
+ kindAttribute: string;
11
+ onOpenLinkedDoc?: (path: string) => void;
12
+ onOpenCodeFile?: (path: string) => void;
13
+ imageBaseDir?: string;
14
+ onImageClick?: (src: string, alt: string) => void;
15
+ githubRepo?: string;
16
+ onNavigateAnchor?: (hash: string) => void;
17
+ }
18
+
19
+ export const Callout: React.FC<CalloutProps> = ({
20
+ blockId,
21
+ kind,
22
+ body,
23
+ containerClassName,
24
+ blockType,
25
+ kindAttribute,
26
+ onOpenLinkedDoc,
27
+ onOpenCodeFile,
28
+ imageBaseDir,
29
+ onImageClick,
30
+ githubRepo,
31
+ onNavigateAnchor,
32
+ }) => {
33
+ const kindAttr =
34
+ blockType === 'alert' ? { 'data-alert-kind': kindAttribute } : { 'data-directive-kind': kindAttribute };
35
+ return (
36
+ <div
37
+ className={containerClassName}
38
+ data-block-id={blockId}
39
+ data-block-type={blockType}
40
+ {...kindAttr}
41
+ >
42
+ <div className={`${blockType}-title text-xs font-semibold uppercase tracking-wide mb-1`}>
43
+ {kind}
44
+ </div>
45
+ {renderProseBody({
46
+ body,
47
+ // Callout inherits text color from the container (directive tint per
48
+ // kind). Only pass size/leading classes, not a text-foreground value.
49
+ paragraphClassName: 'text-[15px] leading-relaxed',
50
+ listClassName: 'text-[15px] leading-relaxed',
51
+ imageBaseDir,
52
+ onImageClick,
53
+ onOpenLinkedDoc,
54
+ onOpenCodeFile,
55
+ onNavigateAnchor,
56
+ githubRepo,
57
+ })}
58
+ </div>
59
+ );
60
+ };
@@ -0,0 +1,75 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import hljs from 'highlight.js';
3
+ import 'highlight.js/styles/github-dark.css';
4
+ import { Block } from '../../types';
5
+
6
+ interface CodeBlockProps {
7
+ block: Block;
8
+ onHover: (element: HTMLElement) => void;
9
+ onLeave: () => void;
10
+ isHovered: boolean;
11
+ }
12
+
13
+ export const CodeBlock: React.FC<CodeBlockProps> = ({ block, onHover, onLeave, isHovered }) => {
14
+ const [copied, setCopied] = useState(false);
15
+ const containerRef = useRef<HTMLDivElement>(null);
16
+ const codeRef = useRef<HTMLElement>(null);
17
+
18
+ // Highlight code block on mount and when content/language changes
19
+ useEffect(() => {
20
+ if (codeRef.current) {
21
+ // Reset any previous highlighting
22
+ codeRef.current.removeAttribute('data-highlighted');
23
+ codeRef.current.className = `hljs font-mono${block.language ? ` language-${block.language}` : ''}`;
24
+ hljs.highlightElement(codeRef.current);
25
+ }
26
+ }, [block.content, block.language]);
27
+
28
+ const handleCopy = useCallback(async () => {
29
+ try {
30
+ await navigator.clipboard.writeText(block.content);
31
+ setCopied(true);
32
+ setTimeout(() => setCopied(false), 2000);
33
+ } catch (err) {
34
+ console.error('Failed to copy:', err);
35
+ }
36
+ }, [block.content]);
37
+
38
+ const handleMouseEnter = () => {
39
+ if (containerRef.current) {
40
+ onHover(containerRef.current);
41
+ }
42
+ };
43
+
44
+ // Build className for code element
45
+ const codeClassName = `hljs font-mono${block.language ? ` language-${block.language}` : ''}`;
46
+
47
+ return (
48
+ <div
49
+ ref={containerRef}
50
+ className="relative group my-5"
51
+ data-block-id={block.id}
52
+ onMouseEnter={handleMouseEnter}
53
+ onMouseLeave={onLeave}
54
+ >
55
+ <button
56
+ onClick={handleCopy}
57
+ className="absolute top-2 right-2 p-1.5 rounded-md bg-muted/80 hover:bg-muted text-muted-foreground hover:text-foreground opacity-0 group-hover:opacity-100 transition-opacity z-10"
58
+ title={copied ? 'Copied!' : 'Copy code'}
59
+ >
60
+ {copied ? (
61
+ <svg className="w-4 h-4 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
62
+ <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
63
+ </svg>
64
+ ) : (
65
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
66
+ <path strokeLinecap="round" strokeLinejoin="round" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
67
+ </svg>
68
+ )}
69
+ </button>
70
+ <pre className="rounded-lg text-[13px] overflow-x-auto bg-muted/50 border border-border/30">
71
+ <code ref={codeRef} className={codeClassName}>{block.content}</code>
72
+ </pre>
73
+ </div>
74
+ );
75
+ };