@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,299 @@
1
+ import React, { useEffect, useRef, useState } from 'react';
2
+ import { ChevronDown, Check, Copy, MoreHorizontal } from 'lucide-react';
3
+ import { AppIcon } from './icons/AppIcon';
4
+ import { getLastOpenInApp, setLastOpenInApp } from '../utils/storage';
5
+ import type { OpenInKind } from '@plannotator/core/open-in-apps';
6
+ import {
7
+ DropdownMenu,
8
+ DropdownMenuTrigger,
9
+ DropdownMenuContent,
10
+ DropdownMenuItem,
11
+ DropdownMenuSeparator,
12
+ } from './ui/dropdown-menu';
13
+
14
+ /**
15
+ * OpenInAppButton — split button + file-actions menu.
16
+ *
17
+ * When the file is launchable (local session, resolvable path), the left half
18
+ * opens it in the last-selected app and the chevron drops down the detected
19
+ * apps (file manager, then editors, then terminals) — picking one persists it
20
+ * AND opens immediately. The menu also carries Copy path and (review only)
21
+ * Copy file diff.
22
+ *
23
+ * When launching is unavailable (remote session, or PR review with no local
24
+ * checkout), the app actions are hidden but the copy actions remain behind a
25
+ * `⋯` overflow button — so e.g. Copy file diff never disappears. Renders
26
+ * nothing when there is neither an app to open nor a diff to copy.
27
+ */
28
+
29
+ interface DetectedApp {
30
+ id: string;
31
+ label: string;
32
+ kind: OpenInKind;
33
+ icon: string;
34
+ }
35
+
36
+ interface OpenInAppButtonProps {
37
+ filePath: string | null | undefined;
38
+ base?: string | null;
39
+ /** Diff/patch text for the "Copy file diff" menu action (review only). */
40
+ diffText?: string | null;
41
+ /**
42
+ * When false, hide the app-launch actions (e.g. PR review with no local
43
+ * checkout, where files aren't resolvable on disk) but keep copy actions.
44
+ */
45
+ canOpen?: boolean;
46
+ /** When true, render the primary label text alongside the icon. */
47
+ showLabel?: boolean;
48
+ disabled?: boolean;
49
+ }
50
+
51
+ // The host app catalog is static for the session, but the all-files view
52
+ // renders one OpenInAppButton per file — so fetch /api/open-in/apps once and
53
+ // share the promise across every instance instead of N identical requests.
54
+ interface OpenInAppsResponse {
55
+ available: boolean;
56
+ apps: DetectedApp[];
57
+ }
58
+ let openInAppsPromise: Promise<OpenInAppsResponse> | null = null;
59
+ function loadOpenInApps(): Promise<OpenInAppsResponse> {
60
+ if (!openInAppsPromise) {
61
+ openInAppsPromise = fetch('/api/open-in/apps')
62
+ .then((r) => r.json())
63
+ .then((data: OpenInAppsResponse) => ({
64
+ available: !!data.available,
65
+ apps: Array.isArray(data.apps) ? data.apps : [],
66
+ }))
67
+ .catch(() => {
68
+ openInAppsPromise = null; // don't memoize failure — let the next mount retry
69
+ return { available: false, apps: [] };
70
+ });
71
+ }
72
+ return openInAppsPromise;
73
+ }
74
+
75
+ export const OpenInAppButton: React.FC<OpenInAppButtonProps> = ({
76
+ filePath,
77
+ base,
78
+ diffText,
79
+ canOpen = true,
80
+ showLabel = false,
81
+ disabled = false,
82
+ }) => {
83
+ const [apps, setApps] = useState<DetectedApp[] | null>(null);
84
+ const [available, setAvailable] = useState<boolean | null>(null);
85
+ const [currentId, setCurrentId] = useState<string>(() => getLastOpenInApp());
86
+ const [busy, setBusy] = useState(false);
87
+ const [error, setError] = useState<string | null>(null);
88
+ const [menuOpen, setMenuOpen] = useState(false);
89
+ const errorTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
90
+
91
+ useEffect(() => {
92
+ let cancelled = false;
93
+ loadOpenInApps().then((data) => {
94
+ if (cancelled) return;
95
+ setAvailable(data.available);
96
+ setApps(data.apps);
97
+ });
98
+ return () => {
99
+ cancelled = true;
100
+ };
101
+ }, []);
102
+
103
+ useEffect(() => {
104
+ return () => {
105
+ if (errorTimer.current) clearTimeout(errorTimer.current);
106
+ };
107
+ }, []);
108
+
109
+ const list = apps ?? [];
110
+ // Apps are launchable: server says available, the surface allows it, the path
111
+ // is resolvable, and at least one app was detected.
112
+ const openable =
113
+ available === true && canOpen && !!filePath && list.length > 0;
114
+
115
+ // Nothing to do: can't open AND nothing to copy. Also preserve the
116
+ // "hide when unavailable" behavior for surfaces whose only copy action is the
117
+ // path (annotate in a remote session) — show the standalone menu only when
118
+ // there's a diff to copy.
119
+ if (!openable && !diffText) return null;
120
+
121
+ // Resolve the active app: last-used if still detected, else reveal, else first.
122
+ const resolvedId = list.some((a) => a.id === currentId)
123
+ ? currentId
124
+ : list.some((a) => a.id === 'reveal')
125
+ ? 'reveal'
126
+ : (list[0]?.id ?? 'reveal');
127
+ const currentApp = list.find((a) => a.id === resolvedId) ?? list[0];
128
+
129
+ const flashError = (msg: string) => {
130
+ setError(msg);
131
+ if (errorTimer.current) clearTimeout(errorTimer.current);
132
+ errorTimer.current = setTimeout(() => setError(null), 4000);
133
+ };
134
+
135
+ const open = async (appId: string) => {
136
+ if (!filePath || busy) return;
137
+ setBusy(true);
138
+ setError(null);
139
+ try {
140
+ const res = await fetch('/api/open-in', {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify({ filePath, base: base ?? null, appId }),
144
+ });
145
+ const data = (await res.json().catch(() => null)) as
146
+ | { ok: boolean; error?: string }
147
+ | null;
148
+ if (!res.ok || !data || data.ok === false) {
149
+ flashError(data?.error || 'Failed to open');
150
+ }
151
+ } catch {
152
+ flashError('Failed to open');
153
+ } finally {
154
+ setBusy(false);
155
+ }
156
+ };
157
+
158
+ const selectApp = (appId: string) => {
159
+ setLastOpenInApp(appId);
160
+ setCurrentId(appId);
161
+ setMenuOpen(false);
162
+ void open(appId);
163
+ };
164
+
165
+ const copyText = async (text: string) => {
166
+ try {
167
+ await navigator.clipboard.writeText(text);
168
+ } catch {
169
+ /* ignore */
170
+ }
171
+ setMenuOpen(false);
172
+ };
173
+
174
+ // Menu groups: file manager first, then editors, then terminals.
175
+ const grouped = [
176
+ list.filter((a) => a.kind === 'file-manager'),
177
+ list.filter((a) => a.kind === 'editor'),
178
+ list.filter((a) => a.kind === 'terminal'),
179
+ ].filter((g) => g.length > 0);
180
+
181
+ const Spinner = (
182
+ <svg className="w-3.5 h-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
183
+ <circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
184
+ <path
185
+ className="opacity-75"
186
+ fill="currentColor"
187
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
188
+ />
189
+ </svg>
190
+ );
191
+
192
+ const isDisabled = disabled || busy;
193
+
194
+ return (
195
+ <div className="flex items-center gap-1">
196
+ <div className="flex items-center rounded overflow-hidden">
197
+ {/* Primary: open in the current app (only when launchable). */}
198
+ {openable && currentApp && (
199
+ <button
200
+ type="button"
201
+ onClick={() => open(resolvedId)}
202
+ disabled={isDisabled}
203
+ className={`text-xs flex items-center gap-1 py-1 transition-colors text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed ${
204
+ showLabel ? 'pl-2 pr-1.5' : 'px-1.5'
205
+ }`}
206
+ title={error ?? currentApp.label}
207
+ aria-label={`Open in ${currentApp.label}`}
208
+ >
209
+ {busy ? Spinner : <AppIcon id={currentApp.icon} className="w-3.5 h-3.5" />}
210
+ {showLabel && <span className="whitespace-nowrap">{currentApp.label}</span>}
211
+ </button>
212
+ )}
213
+ {/* Chevron (with primary) or standalone overflow (copy-only). */}
214
+ <DropdownMenu open={menuOpen} onOpenChange={setMenuOpen}>
215
+ <DropdownMenuTrigger asChild>
216
+ <button
217
+ type="button"
218
+ disabled={isDisabled}
219
+ className={`text-xs flex items-center py-1 transition-colors text-muted-foreground hover:text-foreground hover:bg-muted disabled:opacity-50 disabled:cursor-not-allowed ${
220
+ openable ? 'px-1 border-l border-border/50' : 'px-1.5'
221
+ }`}
222
+ title={openable ? 'Open in…' : 'File actions'}
223
+ aria-label={openable ? 'Choose app to open in' : 'File actions'}
224
+ >
225
+ {openable ? (
226
+ <ChevronDown className="w-3 h-3" />
227
+ ) : (
228
+ <MoreHorizontal className="w-3.5 h-3.5" />
229
+ )}
230
+ </button>
231
+ </DropdownMenuTrigger>
232
+ <DropdownMenuContent
233
+ align="end"
234
+ sideOffset={6}
235
+ className="min-w-[12rem]"
236
+ // Don't snap focus (and its focus ring) back onto the trigger when
237
+ // the menu closes — that left-edge bar reads as a stray artifact.
238
+ onCloseAutoFocus={(event) => event.preventDefault()}
239
+ >
240
+ {openable &&
241
+ grouped.map((group, gi) => (
242
+ <React.Fragment key={group[0].kind}>
243
+ {gi > 0 && <DropdownMenuSeparator />}
244
+ {group.map((app) => (
245
+ <DropdownMenuItem
246
+ key={app.id}
247
+ onSelect={(e) => {
248
+ e.preventDefault();
249
+ selectApp(app.id);
250
+ }}
251
+ className="text-xs"
252
+ >
253
+ <AppIcon id={app.icon} className="w-4 h-4" />
254
+ <span className="flex-1 truncate">{app.label}</span>
255
+ {app.id === resolvedId && <Check className="w-3.5 h-3.5 text-foreground" />}
256
+ </DropdownMenuItem>
257
+ ))}
258
+ </React.Fragment>
259
+ ))}
260
+ {openable && (filePath || diffText) && <DropdownMenuSeparator />}
261
+ {filePath && (
262
+ <DropdownMenuItem
263
+ onSelect={(e) => {
264
+ e.preventDefault();
265
+ void copyText(filePath);
266
+ }}
267
+ className="text-xs"
268
+ >
269
+ <Copy className="w-4 h-4" />
270
+ <span className="flex-1">Copy path</span>
271
+ </DropdownMenuItem>
272
+ )}
273
+ {diffText && (
274
+ <DropdownMenuItem
275
+ onSelect={(e) => {
276
+ e.preventDefault();
277
+ void copyText(diffText);
278
+ }}
279
+ className="text-xs"
280
+ >
281
+ <Copy className="w-4 h-4" />
282
+ <span className="flex-1">Copy file diff</span>
283
+ </DropdownMenuItem>
284
+ )}
285
+ </DropdownMenuContent>
286
+ </DropdownMenu>
287
+ </div>
288
+ {error && (
289
+ <span
290
+ role="status"
291
+ aria-live="polite"
292
+ className="text-xs text-destructive truncate max-w-[12rem]"
293
+ >
294
+ {error}
295
+ </span>
296
+ )}
297
+ </div>
298
+ );
299
+ };
@@ -0,0 +1,82 @@
1
+ import {
2
+ forwardRef,
3
+ useCallback,
4
+ useImperativeHandle,
5
+ useRef,
6
+ type CSSProperties,
7
+ type ElementType,
8
+ type ReactNode,
9
+ } from 'react';
10
+
11
+ /**
12
+ * Native scroll container.
13
+ *
14
+ * Was an `overlayscrollbars-react` wrapper (#509 "Zed-style overlay scrollbars").
15
+ * Replaced with **native OS scrollbars** — the element you render IS the scroll node,
16
+ * so the deferred-init event dance, the force-recompute ResizeObserver, and the
17
+ * inner-viewport indirection are all gone. Scrollbar appearance is native, styled
18
+ * globally via `scrollbar-width: thin` + `scrollbar-color` in theme.css/styles.css.
19
+ *
20
+ * The public API is preserved on purpose (name, `onViewportReady`, the imperative
21
+ * handle) so existing consumers don't change — they just receive the host element,
22
+ * which now genuinely is the scrolling element.
23
+ *
24
+ * NOTE: do NOT reintroduce a `::-webkit-scrollbar { width: 6px }` rule — that thin
25
+ * rail was the original "can't grab the scrollbar" bug (#354).
26
+ */
27
+ export interface OverlayScrollAreaHandle {
28
+ /** The DOM element that scrolls, or null before mount. */
29
+ getViewport(): HTMLElement | null;
30
+ }
31
+
32
+ export interface OverlayScrollAreaProps
33
+ extends Omit<React.HTMLAttributes<HTMLElement>, 'children'> {
34
+ /** Root element tag (default 'div'). Use 'main' for the primary plan viewport. */
35
+ element?: ElementType;
36
+ children?: ReactNode;
37
+ /** Fires with the scroll element on mount, and `null` on unmount. */
38
+ onViewportReady?: (viewport: HTMLElement | null) => void;
39
+ /** Horizontal overflow (default 'hidden'). */
40
+ overflowX?: 'hidden' | 'scroll' | 'visible' | 'auto';
41
+ /** Vertical overflow (default 'auto'). */
42
+ overflowY?: 'hidden' | 'scroll' | 'visible' | 'auto';
43
+ }
44
+
45
+ export const OverlayScrollArea = forwardRef<
46
+ OverlayScrollAreaHandle,
47
+ OverlayScrollAreaProps
48
+ >(function OverlayScrollArea(
49
+ {
50
+ element = 'div',
51
+ children,
52
+ onViewportReady,
53
+ overflowX = 'hidden',
54
+ overflowY = 'auto',
55
+ style,
56
+ ...rest
57
+ },
58
+ ref,
59
+ ) {
60
+ const elRef = useRef<HTMLElement | null>(null);
61
+ // Hold the latest callback without changing the ref-callback identity, so the
62
+ // element isn't detached/reattached on every parent render.
63
+ const onReadyRef = useRef(onViewportReady);
64
+ onReadyRef.current = onViewportReady;
65
+
66
+ const getViewport = useCallback((): HTMLElement | null => elRef.current, []);
67
+ useImperativeHandle(ref, () => ({ getViewport }), [getViewport]);
68
+
69
+ const setEl = useCallback((node: HTMLElement | null) => {
70
+ elRef.current = node;
71
+ onReadyRef.current?.(node);
72
+ }, []);
73
+
74
+ const Comp = element as ElementType;
75
+ const mergedStyle: CSSProperties = { overflowX, overflowY, ...style };
76
+
77
+ return (
78
+ <Comp ref={setEl as React.Ref<HTMLElement>} style={mergedStyle} {...rest}>
79
+ {children}
80
+ </Comp>
81
+ );
82
+ });
@@ -0,0 +1,92 @@
1
+ import React, { useState } from 'react';
2
+ import { createPortal } from 'react-dom';
3
+ import {
4
+ PermissionMode,
5
+ PERMISSION_MODE_OPTIONS,
6
+ savePermissionModeSettings,
7
+ } from '../utils/permissionMode';
8
+
9
+ interface PermissionModeSetupProps {
10
+ isOpen: boolean;
11
+ onComplete: (mode: PermissionMode) => void;
12
+ }
13
+
14
+ export const PermissionModeSetup: React.FC<PermissionModeSetupProps> = ({
15
+ isOpen,
16
+ onComplete,
17
+ }) => {
18
+ const [selectedMode, setSelectedMode] = useState<PermissionMode>('acceptEdits');
19
+
20
+ if (!isOpen) return null;
21
+
22
+ const handleConfirm = () => {
23
+ savePermissionModeSettings(selectedMode);
24
+ onComplete(selectedMode);
25
+ };
26
+
27
+ return createPortal(
28
+ <div className="fixed inset-0 z-[100] flex items-center justify-center bg-background/90 backdrop-blur-sm p-4">
29
+ <div className="bg-card border border-border rounded-xl w-full max-w-lg shadow-2xl">
30
+ {/* Header */}
31
+ <div className="p-5 border-b border-border">
32
+ <div className="flex items-center gap-2 mb-2">
33
+ <div className="p-1.5 rounded-lg bg-primary/15">
34
+ <svg className="w-5 h-5 text-primary" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
35
+ <path strokeLinecap="round" strokeLinejoin="round" d="M14.857 17.082a23.848 23.848 0 005.454-1.31A8.967 8.967 0 0118 9.75v-.7V9A6 6 0 006 9v.75a8.967 8.967 0 01-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 01-5.714 0m5.714 0a3 3 0 11-5.714 0" />
36
+ </svg>
37
+ </div>
38
+ <h3 className="font-semibold text-base">New: Permission Mode Preservation</h3>
39
+ </div>
40
+ <p className="text-sm text-muted-foreground">
41
+ Claude Code 2.1.7+ supports preserving your permission mode after plan approval.
42
+ Choose your preferred automation level.
43
+ </p>
44
+ <p className="text-xs text-muted-foreground/70 mt-1">
45
+ Requires Claude Code 2.1.7 or later. Run <code className="bg-muted px-1 rounded">claude update</code> to update.
46
+ </p>
47
+ </div>
48
+
49
+ {/* Options */}
50
+ <div className="p-4 space-y-2">
51
+ {PERMISSION_MODE_OPTIONS.map((option) => (
52
+ <label
53
+ key={option.value}
54
+ className={`flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-all border ${
55
+ selectedMode === option.value
56
+ ? 'border-primary bg-primary/10'
57
+ : 'border-transparent bg-muted/50 hover:bg-muted'
58
+ }`}
59
+ >
60
+ <input
61
+ type="radio"
62
+ name="permissionMode"
63
+ value={option.value}
64
+ checked={selectedMode === option.value}
65
+ onChange={() => setSelectedMode(option.value)}
66
+ className="mt-0.5 accent-primary"
67
+ />
68
+ <div className="flex-1">
69
+ <div className="text-sm font-medium">{option.label}</div>
70
+ <div className="text-xs text-muted-foreground">{option.description}</div>
71
+ </div>
72
+ </label>
73
+ ))}
74
+ </div>
75
+
76
+ {/* Footer */}
77
+ <div className="p-4 border-t border-border flex justify-between items-center">
78
+ <p className="text-xs text-muted-foreground">
79
+ You can change this later in Settings.
80
+ </p>
81
+ <button
82
+ onClick={handleConfirm}
83
+ className="px-4 py-2 bg-primary text-primary-foreground rounded-lg text-sm font-medium hover:opacity-90 transition-opacity"
84
+ >
85
+ Continue
86
+ </button>
87
+ </div>
88
+ </div>
89
+ </div>,
90
+ document.body
91
+ );
92
+ };
@@ -0,0 +1,99 @@
1
+ import React, { useLayoutEffect, useState, useRef } from 'react';
2
+ import { useScrollViewport } from '../hooks/useScrollViewport';
3
+
4
+ interface PinpointOverlayProps {
5
+ target: { element: HTMLElement; label: string } | null;
6
+ containerRef: React.RefObject<HTMLElement | null>;
7
+ }
8
+
9
+ interface OverlayPosition {
10
+ top: number;
11
+ left: number;
12
+ width: number;
13
+ height: number;
14
+ }
15
+
16
+ export const PinpointOverlay: React.FC<PinpointOverlayProps> = ({ target, containerRef }) => {
17
+ const [position, setPosition] = useState<OverlayPosition | null>(null);
18
+ const rafRef = useRef<number>(0);
19
+ const scrollViewport = useScrollViewport();
20
+
21
+ useLayoutEffect(() => {
22
+ if (!target || !containerRef.current) {
23
+ setPosition(null);
24
+ return;
25
+ }
26
+
27
+ const updatePosition = () => {
28
+ const containerRect = containerRef.current!.getBoundingClientRect();
29
+ const targetRect = target.element.getBoundingClientRect();
30
+
31
+ setPosition({
32
+ top: targetRect.top - containerRect.top,
33
+ left: targetRect.left - containerRect.left,
34
+ width: targetRect.width,
35
+ height: targetRect.height,
36
+ });
37
+ };
38
+
39
+ updatePosition();
40
+
41
+ const handleUpdate = () => {
42
+ cancelAnimationFrame(rafRef.current);
43
+ rafRef.current = requestAnimationFrame(updatePosition);
44
+ };
45
+
46
+ // Window resize always matters, regardless of whether the scroll
47
+ // viewport is ready yet. Register it unconditionally so the overlay
48
+ // stays aligned during the brief window before OverlayScrollbars
49
+ // delivers its viewport.
50
+ window.addEventListener('resize', handleUpdate, { passive: true });
51
+
52
+ // The scroll element is the OverlayScrollArea viewport. Falling back to
53
+ // <main> or window would attach to the wrong node and the overlay
54
+ // position would drift silently on scroll.
55
+ scrollViewport?.addEventListener('scroll', handleUpdate, { passive: true });
56
+
57
+ return () => {
58
+ cancelAnimationFrame(rafRef.current);
59
+ window.removeEventListener('resize', handleUpdate);
60
+ scrollViewport?.removeEventListener('scroll', handleUpdate);
61
+ };
62
+ }, [target, containerRef, scrollViewport]);
63
+
64
+ if (!position || !target) return null;
65
+
66
+ return (
67
+ <>
68
+ {/* Outline box */}
69
+ <div
70
+ className="border-2 border-dashed border-primary/50 bg-primary/5 rounded"
71
+ style={{
72
+ position: 'absolute',
73
+ top: position.top - 2,
74
+ left: position.left - 2,
75
+ width: position.width + 4,
76
+ height: position.height + 4,
77
+ pointerEvents: 'none',
78
+ zIndex: 20,
79
+ transition: 'all 100ms ease-out',
80
+ }}
81
+ />
82
+ {/* Label badge */}
83
+ <div
84
+ style={{
85
+ position: 'absolute',
86
+ top: position.top - 22,
87
+ left: position.left - 2,
88
+ pointerEvents: 'none',
89
+ zIndex: 21,
90
+ transition: 'all 100ms ease-out',
91
+ }}
92
+ >
93
+ <span className="inline-block text-[10px] leading-4 px-1.5 rounded-sm bg-primary text-primary-foreground font-mono whitespace-nowrap max-w-[220px] overflow-hidden text-ellipsis">
94
+ {target.label}
95
+ </span>
96
+ </div>
97
+ </>
98
+ );
99
+ };