@plannotator/ui 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -0
- package/assets/diff-options.png +0 -0
- package/assets/icon-codex.png +0 -0
- package/assets/look-flat.png +0 -0
- package/assets/look-grid.png +0 -0
- package/assets/review-sections.png +0 -0
- package/assets/review-tree.png +0 -0
- package/assets/workspaces.webp +0 -0
- package/components/AISettingsTab.tsx +145 -0
- package/components/ActionMenu.tsx +101 -0
- package/components/AgentControls.tsx +216 -0
- package/components/AgentsTab.tsx +1294 -0
- package/components/AnnotationPanel.tsx +731 -0
- package/components/AnnotationSidebar.tsx +86 -0
- package/components/AnnotationToolbar.tsx +323 -0
- package/components/AnnotationToolstrip.tsx +355 -0
- package/components/ApproveDropdown.tsx +170 -0
- package/components/AttachmentsButton.tsx +410 -0
- package/components/BlockRenderer.tsx +163 -0
- package/components/BorderTrail.tsx +38 -0
- package/components/CodeFilePicker.tsx +64 -0
- package/components/CodeFilePopout.tsx +602 -0
- package/components/CodePathValidationContext.tsx +14 -0
- package/components/CommentPopover.tsx +512 -0
- package/components/CompletionOverlay.tsx +99 -0
- package/components/ConfirmDialog.tsx +128 -0
- package/components/DocBadges.tsx +231 -0
- package/components/EditorAnnotationCard.tsx +65 -0
- package/components/ExportModal.tsx +565 -0
- package/components/FloatingQuickLabelPicker.tsx +144 -0
- package/components/GitHubIcon.tsx +16 -0
- package/components/GitLabIcon.tsx +30 -0
- package/components/GraphvizBlock.tsx +511 -0
- package/components/ImageAnnotator/Canvas.tsx +118 -0
- package/components/ImageAnnotator/Toolbar.tsx +218 -0
- package/components/ImageAnnotator/index.tsx +272 -0
- package/components/ImageAnnotator/types.ts +39 -0
- package/components/ImageAnnotator/utils.ts +147 -0
- package/components/ImageThumbnail.tsx +126 -0
- package/components/ImportModal.tsx +146 -0
- package/components/InlineMarkdown.tsx +1094 -0
- package/components/KeyboardShortcuts.tsx +208 -0
- package/components/Landing.tsx +496 -0
- package/components/ListItemBody.tsx +62 -0
- package/components/ListMarker.tsx +73 -0
- package/components/LookAndFeelAnnouncementDialog.tsx +230 -0
- package/components/MarkdownEditor.tsx +44 -0
- package/components/MenuVersionSection.tsx +91 -0
- package/components/MermaidBlock.tsx +579 -0
- package/components/ModeToggle.tsx +68 -0
- package/components/OpenInAppButton.tsx +299 -0
- package/components/OverlayScrollArea.tsx +82 -0
- package/components/PermissionModeSetup.tsx +92 -0
- package/components/PinpointOverlay.tsx +99 -0
- package/components/PlanAIAnnouncementDialog.tsx +187 -0
- package/components/PlanHeaderMenu.tsx +299 -0
- package/components/PopoutDialog.tsx +91 -0
- package/components/Popover.tsx +27 -0
- package/components/ProviderIcons.tsx +51 -0
- package/components/PullRequestIcon.tsx +12 -0
- package/components/QuickLabelDropdown.tsx +63 -0
- package/components/RenderedMarkdown.tsx +54 -0
- package/components/RepoIcon.tsx +12 -0
- package/components/ResizeHandle.tsx +86 -0
- package/components/ReviewAgentsIcon.tsx +18 -0
- package/components/SearchableSelect.tsx +157 -0
- package/components/Settings.tsx +2220 -0
- package/components/SparklesIcon.tsx +45 -0
- package/components/StickyHeaderLane.tsx +260 -0
- package/components/TableOfContents.tsx +157 -0
- package/components/TaterSpritePullup.tsx +34 -0
- package/components/TaterSpriteRunning.tsx +54 -0
- package/components/TaterSpriteSitting.tsx +37 -0
- package/components/TextShimmer.tsx +57 -0
- package/components/ThemeProvider.tsx +162 -0
- package/components/ThemeTab.tsx +139 -0
- package/components/ToolbarButtons.tsx +116 -0
- package/components/Tooltip.tsx +45 -0
- package/components/Viewer.tsx +977 -0
- package/components/ai/AIProviderBar.tsx +95 -0
- package/components/ai/DocumentAIChatPanel.tsx +313 -0
- package/components/blocks/AlertBlock.tsx +58 -0
- package/components/blocks/Callout.tsx +60 -0
- package/components/blocks/CodeBlock.tsx +75 -0
- package/components/blocks/HtmlBlock.tsx +122 -0
- package/components/blocks/MathBlock.tsx +36 -0
- package/components/blocks/TableBlock.tsx +146 -0
- package/components/blocks/TablePopout.tsx +279 -0
- package/components/blocks/TableToolbar.tsx +153 -0
- package/components/blocks/proseBody.tsx +102 -0
- package/components/core/button.tsx +44 -0
- package/components/core/textarea.tsx +26 -0
- package/components/diagramLanguages.ts +14 -0
- package/components/goal-setup/GoalSetupSurface.tsx +1354 -0
- package/components/html-viewer/HtmlViewer.tsx +386 -0
- package/components/html-viewer/bridge-script.ts +505 -0
- package/components/html-viewer/index.ts +1 -0
- package/components/html-viewer/useHtmlAnnotation.ts +400 -0
- package/components/icons/AgentIcons.tsx +66 -0
- package/components/icons/AppIcon.tsx +56 -0
- package/components/icons/MessagesIcon.tsx +11 -0
- package/components/icons/ObsidianIcons.tsx +208 -0
- package/components/icons/app/android-studio.svg +369 -0
- package/components/icons/app/antigravity.svg +97 -0
- package/components/icons/app/cursor.svg +16 -0
- package/components/icons/app/file-explorer.svg +20 -0
- package/components/icons/app/finder.png +0 -0
- package/components/icons/app/ghostty.svg +13 -0
- package/components/icons/app/iterm2.svg +13 -0
- package/components/icons/app/powershell.svg +14 -0
- package/components/icons/app/sublime-text.svg +17 -0
- package/components/icons/app/terminal.png +0 -0
- package/components/icons/app/textmate.png +0 -0
- package/components/icons/app/vscode.svg +39 -0
- package/components/icons/app/warp.png +0 -0
- package/components/icons/app/xcode.png +0 -0
- package/components/icons/app/zed-dark.svg +15 -0
- package/components/icons/app/zed.svg +15 -0
- package/components/icons/themeIcons.tsx +47 -0
- package/components/mermaidSvg.ts +33 -0
- package/components/plan-diff/PlanCleanDiffView.tsx +884 -0
- package/components/plan-diff/PlanDiffBadge.tsx +48 -0
- package/components/plan-diff/PlanDiffModeSwitcher.tsx +103 -0
- package/components/plan-diff/PlanDiffViewer.tsx +214 -0
- package/components/plan-diff/PlanRawDiffView.tsx +102 -0
- package/components/plan-diff/VSCodeIcon.tsx +132 -0
- package/components/settings/HooksTab.tsx +208 -0
- package/components/sidebar/ArchiveBrowser.tsx +98 -0
- package/components/sidebar/CountBadge.tsx +12 -0
- package/components/sidebar/FileBrowser.tsx +498 -0
- package/components/sidebar/MessagesBrowser.tsx +109 -0
- package/components/sidebar/SidebarContainer.tsx +353 -0
- package/components/sidebar/SidebarTabs.tsx +155 -0
- package/components/sidebar/VersionBrowser.tsx +141 -0
- package/components/types.d.ts +7 -0
- package/components/ui/badge.tsx +44 -0
- package/components/ui/button.tsx +83 -0
- package/components/ui/card.tsx +58 -0
- package/components/ui/dialog.tsx +106 -0
- package/components/ui/dropdown-menu.tsx +240 -0
- package/components/ui/state-pill.tsx +45 -0
- package/components/ui/tabs.tsx +46 -0
- package/components/ui/textarea.tsx +25 -0
- package/config/configStore.ts +214 -0
- package/config/index.ts +4 -0
- package/config/reviewView.ts +42 -0
- package/config/settings.ts +310 -0
- package/config/useConfig.ts +20 -0
- package/configure.ts +69 -0
- package/globals.d.ts +14 -0
- package/hooks/pfm/useCodeFilePopout.ts +111 -0
- package/hooks/useAIChat.ts +532 -0
- package/hooks/useAIProviderConfig.ts +115 -0
- package/hooks/useActiveSection.ts +78 -0
- package/hooks/useAgentJobs.ts +306 -0
- package/hooks/useAgentSettings.ts +579 -0
- package/hooks/useAgents.ts +90 -0
- package/hooks/useAnnotationDraft.ts +523 -0
- package/hooks/useAnnotationHighlighter.ts +1170 -0
- package/hooks/useArchive.ts +171 -0
- package/hooks/useAutoClose.ts +104 -0
- package/hooks/useCodeAnnotationDraft.ts +194 -0
- package/hooks/useCodeFilePopout.ts +1 -0
- package/hooks/useDismissOnOutsideAndEscape.ts +50 -0
- package/hooks/useDraggable.ts +108 -0
- package/hooks/useEditorAnnotations.ts +64 -0
- package/hooks/useExternalAnnotationHighlights.ts +105 -0
- package/hooks/useExternalAnnotations.ts +268 -0
- package/hooks/useFileBrowser.ts +402 -0
- package/hooks/useInputMethodSwitch.ts +89 -0
- package/hooks/useIsMobile.ts +17 -0
- package/hooks/useLinkedDoc.ts +494 -0
- package/hooks/useOverlayViewport.ts +36 -0
- package/hooks/usePinpoint.ts +182 -0
- package/hooks/usePlanDiff.ts +176 -0
- package/hooks/usePrintMode.ts +27 -0
- package/hooks/useResizablePanel.ts +173 -0
- package/hooks/useScrollViewport.ts +38 -0
- package/hooks/useSharing.ts +441 -0
- package/hooks/useSidebar.ts +55 -0
- package/hooks/useUpdateCheck.ts +132 -0
- package/hooks/useValidatedCodePaths.ts +94 -0
- package/icons/GitUser.tsx +17 -0
- package/lib/utils.ts +11 -0
- package/package.json +117 -0
- package/plannotator.webp +0 -0
- package/print.css +427 -0
- package/shortcuts/code-review/ai.shortcuts.ts +25 -0
- package/shortcuts/code-review/allFilesDiff.shortcuts.ts +46 -0
- package/shortcuts/code-review/annotationToolbar.shortcuts.ts +30 -0
- package/shortcuts/code-review/fileTree.shortcuts.ts +35 -0
- package/shortcuts/code-review/prComments.shortcuts.ts +18 -0
- package/shortcuts/code-review/suggestionModal.shortcuts.ts +25 -0
- package/shortcuts/code-review/tourDialog.shortcuts.ts +18 -0
- package/shortcuts/core.ts +399 -0
- package/shortcuts/index.ts +21 -0
- package/shortcuts/plan-review/annotationPanel.shortcuts.ts +25 -0
- package/shortcuts/plan-review/annotationToolbar.shortcuts.ts +39 -0
- package/shortcuts/plan-review/commentPopover.shortcuts.ts +21 -0
- package/shortcuts/plan-review/goalSetup.shortcuts.ts +10 -0
- package/shortcuts/plan-review/imageAnnotator.shortcuts.ts +42 -0
- package/shortcuts/plan-review/inputMethod.shortcuts.ts +30 -0
- package/shortcuts/plan-review/sidebar.shortcuts.ts +34 -0
- package/shortcuts/plan-review/viewer.shortcuts.ts +24 -0
- package/shortcuts/runtime.ts +259 -0
- package/sprite_package_additional/index.html +34 -0
- package/sprite_package_additional/sprite.png +0 -0
- package/sprite_package_new/index.html +34 -0
- package/sprite_package_new/sprite.png +0 -0
- package/sprite_package_pulluphang/index.html +34 -0
- package/sprite_package_pulluphang/sprite.png +0 -0
- package/styles.css +1 -0
- package/theme.css +893 -0
- package/themes/adwaita.css +62 -0
- package/themes/andromeeda.css +66 -0
- package/themes/aurora-x.css +66 -0
- package/themes/ayu-dark.css +66 -0
- package/themes/caffeine.css +113 -0
- package/themes/catppuccin.css +62 -0
- package/themes/claude-plus.css +60 -0
- package/themes/cursor-hc.css +34 -0
- package/themes/cursor-midnight.css +34 -0
- package/themes/cursor.css +62 -0
- package/themes/dark-plus.css +66 -0
- package/themes/doom-64.css +109 -0
- package/themes/dracula.css +33 -0
- package/themes/everforest-hard.css +62 -0
- package/themes/everforest-soft.css +62 -0
- package/themes/everforest.css +62 -0
- package/themes/github.css +66 -0
- package/themes/gruvbox.css +62 -0
- package/themes/houston.css +66 -0
- package/themes/kanagawa-dragon.css +34 -0
- package/themes/kanagawa-lotus.css +34 -0
- package/themes/kanagawa-wave.css +34 -0
- package/themes/laserwave.css +66 -0
- package/themes/material.css +62 -0
- package/themes/min.css +66 -0
- package/themes/monokai-pro.css +34 -0
- package/themes/neutral.css +59 -0
- package/themes/night-owl.css +66 -0
- package/themes/nord.css +66 -0
- package/themes/one-dark-pro.css +66 -0
- package/themes/one-light.css +66 -0
- package/themes/paulmillr.css +34 -0
- package/themes/plannotator.css +60 -0
- package/themes/plastic.css +66 -0
- package/themes/poimandres.css +66 -0
- package/themes/quantum-rose.css +109 -0
- package/themes/red.css +66 -0
- package/themes/rose-pine.css +62 -0
- package/themes/simple.css +124 -0
- package/themes/slack.css +66 -0
- package/themes/snazzy-light.css +66 -0
- package/themes/soft-pop.css +60 -0
- package/themes/solar-dusk.css +109 -0
- package/themes/solarized.css +66 -0
- package/themes/synthwave-84.css +34 -0
- package/themes/terminal.css +62 -0
- package/themes/tinacious.css +57 -0
- package/themes/tokyo-night.css +62 -0
- package/themes/vesper.css +62 -0
- package/themes/vitesse-black.css +66 -0
- package/themes/vitesse.css +62 -0
- package/types.ts +264 -0
- package/utils/agentSwitch.ts +71 -0
- package/utils/aiChatFormat.ts +33 -0
- package/utils/aiPrompt.ts +29 -0
- package/utils/aiProvider.ts +217 -0
- package/utils/anchors.ts +11 -0
- package/utils/annotateAgentTerminal.ts +29 -0
- package/utils/annotationHelpers.ts +101 -0
- package/utils/bear.ts +59 -0
- package/utils/blockTargeting.ts +240 -0
- package/utils/callback.ts +99 -0
- package/utils/commentContent.ts +8 -0
- package/utils/defaultNotesApp.ts +20 -0
- package/utils/diffFonts.ts +33 -0
- package/utils/editorMode.ts +31 -0
- package/utils/fileBrowser.ts +40 -0
- package/utils/generateId.ts +7 -0
- package/utils/generateIdentity.ts +29 -0
- package/utils/identity.ts +112 -0
- package/utils/inlineTransforms.ts +41 -0
- package/utils/inputMethod.ts +17 -0
- package/utils/lookAndFeelAnnouncement.ts +18 -0
- package/utils/obsidian.ts +201 -0
- package/utils/octarine.ts +53 -0
- package/utils/parser.ts +991 -0
- package/utils/permissionMode.ts +77 -0
- package/utils/planAIAnnouncement.ts +17 -0
- package/utils/planAgentInstructions.ts +137 -0
- package/utils/planDiffEngine.ts +590 -0
- package/utils/planSave.ts +49 -0
- package/utils/platform.ts +11 -0
- package/utils/quickLabels.ts +77 -0
- package/utils/reviewAgentInstructions.ts +182 -0
- package/utils/sanitizeHtml.ts +45 -0
- package/utils/sharing.ts +350 -0
- package/utils/slugify.ts +37 -0
- package/utils/storage.ts +141 -0
- package/utils/themeRegistry.ts +567 -0
- package/utils/uiPreferences.ts +34 -0
- package/utils/upload.ts +56 -0
- package/utils/wideMode.ts +48 -0
|
@@ -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
|
+
};
|