diff --git a/bun.lock b/bun.lock index 1a4e0fe0..fbeb5788 100644 --- a/bun.lock +++ b/bun.lock @@ -170,6 +170,8 @@ "name": "@plannotator/editor", "version": "0.0.1", "dependencies": { + "@fontsource-variable/geist-mono": "5.2.7", + "@fontsource-variable/inter": "^5.2.8", "@plannotator/ai": "workspace:*", "@plannotator/shared": "workspace:*", "@plannotator/ui": "workspace:*", @@ -177,12 +179,15 @@ "react-dom": "^19.2.3", "sonner": "^2.0.7", "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", }, }, "packages/review-editor": { "name": "@plannotator/review-editor", "version": "0.0.1", "dependencies": { + "@fontsource-variable/geist-mono": "5.2.7", + "@fontsource-variable/inter": "^5.2.8", "@pierre/diffs": "^1.1.12", "@plannotator/shared": "workspace:*", "@plannotator/ui": "workspace:*", @@ -194,10 +199,12 @@ "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-tooltip": "^1.2.8", "highlight.js": "^11.11.1", + "lucide-react": "^1.14.0", "motion": "^12.38.0", "react": "^19.2.3", "react-dom": "^19.2.3", "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", }, }, "packages/server": { @@ -227,26 +234,34 @@ "name": "@plannotator/ui", "version": "0.0.1", "dependencies": { + "@fontsource-variable/geist-mono": "5.2.7", + "@fontsource-variable/inter": "^5.2.8", "@pierre/diffs": "^1.1.12", "@plannotator/ai": "workspace:*", "@plannotator/shared": "workspace:*", "@plannotator/web-highlighter": "^0.8.1", "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-popover": "^1.1.15", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-table": "^8.21.3", "@viz-js/viz": "^3.25.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "diff": "^8.0.3", "highlight.js": "^11.11.1", "lucide-react": "^1.14.0", "marked": "^17.0.6", "mermaid": "^11.12.2", "motion": "^12.38.0", - "overlayscrollbars": "^2.11.0", - "overlayscrollbars-react": "^0.5.6", "perfect-freehand": "^1.2.2", "react": "^19.2.3", "react-dom": "^19.2.3", + "tailwind-merge": "^3.6.0", + "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7", "unique-username-generator": "^1.5.1", }, "devDependencies": { @@ -530,6 +545,10 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.11", "", {}, "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg=="], + "@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.2.7", "", {}, "sha512-ZKlZ5sjtalb2TwXKs400mAGDlt/+2ENLNySPx0wTz3bP3mWARCsUW+rpxzZc7e05d2qGch70pItt3K4qttbIYA=="], + + "@fontsource-variable/inter": ["@fontsource-variable/inter@5.2.8", "", {}, "sha512-kOfP2D+ykbcX/P3IFnokOhVRNoTozo5/JxhAIVYLpea/UBmCQ/YWPBfWIDuBImXX/15KH+eKh4xpEUyS2sQQGQ=="], + "@google/genai": ["@google/genai@1.42.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^4.6.2", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-+3nlMTcrQufbQ8IumGkOphxD5Pd5kKyJOzLcnY0/1IuE8upJk5aLmoexZ2BJhBp1zAjRJMEB4a2CJwKI9e2EYw=="], "@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="], @@ -786,7 +805,9 @@ "@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="], - "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="], + + "@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="], "@radix-ui/react-tooltip": ["@radix-ui/react-tooltip@1.2.8", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg=="], @@ -1322,6 +1343,8 @@ "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], @@ -2148,10 +2171,6 @@ "openai": ["openai@6.26.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA=="], - "overlayscrollbars": ["overlayscrollbars@2.14.0", "", {}, "sha512-RjV0pqc79kYhQLC3vTcLRb5GLpI1n6qh0Oua3g+bGH4EgNOJHVBGP7u0zZtxoAa0dkHlAqTTSYRb9MMmxNLjig=="], - - "overlayscrollbars-react": ["overlayscrollbars-react@0.5.6", "", { "peerDependencies": { "overlayscrollbars": "^2.0.0", "react": ">=16.8.0" } }, "sha512-E5To04bL5brn9GVCZ36SnfGanxa2I2MDkWoa4Cjo5wol7l+diAgi4DBc983V7l2nOk/OLJ6Feg4kySspQEGDBw=="], - "p-limit": ["p-limit@6.2.0", "", { "dependencies": { "yocto-queue": "^1.1.1" } }, "sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA=="], "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], @@ -2476,8 +2495,12 @@ "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], + "tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="], + "tailwindcss-animate": ["tailwindcss-animate@1.0.7", "", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="], + "tapable": ["tapable@2.3.0", "", {}, "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg=="], "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], @@ -2754,6 +2777,18 @@ "@plannotator/review/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], + "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-dialog/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-menu/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-popover/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], diff --git a/packages/editor/App.tsx b/packages/editor/App.tsx index e816c1e7..d6a799ca 100644 --- a/packages/editor/App.tsx +++ b/packages/editor/App.tsx @@ -22,10 +22,11 @@ import { getCallbackConfig, CallbackAction, executeCallback } from '@plannotator import { useAgents } from '@plannotator/ui/hooks/useAgents'; import { useActiveSection } from '@plannotator/ui/hooks/useActiveSection'; import { storage } from '@plannotator/ui/utils/storage'; -import { configStore } from '@plannotator/ui/config'; +import { configStore, useConfigValue } from '@plannotator/ui/config'; import { CompletionOverlay } from '@plannotator/ui/components/CompletionOverlay'; import { useUpdateCheck } from '@plannotator/ui/hooks/useUpdateCheck'; import { PlanAIAnnouncementDialog } from '@plannotator/ui/components/PlanAIAnnouncementDialog'; +import { LookAndFeelAnnouncementDialog } from '@plannotator/ui/components/LookAndFeelAnnouncementDialog'; import { getObsidianSettings, getEffectiveVaultPath, isObsidianConfigured, CUSTOM_PATH_SENTINEL } from '@plannotator/ui/utils/obsidian'; import { getBearSettings } from '@plannotator/ui/utils/bear'; import { getOctarineSettings, isOctarineConfigured } from '@plannotator/ui/utils/octarine'; @@ -39,6 +40,7 @@ import { saveAIProviderSelection, } from '@plannotator/ui/utils/aiProvider'; import { markPlanAIAnnouncementSeen, needsPlanAIAnnouncement } from '@plannotator/ui/utils/planAIAnnouncement'; +import { markLookAndFeelAnnouncementSeen, needsLookAndFeelAnnouncement } from '@plannotator/ui/utils/lookAndFeelAnnouncement'; import { useAIChat } from '@plannotator/ui/hooks/useAIChat'; import { getUIPreferences, type UIPreferences, type PlanWidth } from '@plannotator/ui/utils/uiPreferences'; import { getEditorMode, saveEditorMode } from '@plannotator/ui/utils/editorMode'; @@ -206,6 +208,7 @@ const App: React.FC = () => { const stored = storage.getItem('plannotator-tater-mode'); return stored === 'true'; }); + const gridEnabled = useConfigValue('gridEnabled'); const [uiPrefs, setUiPrefs] = useState(() => getUIPreferences()); // Plan-area width (inside the OverlayScrollArea, after sidebar/panel @@ -259,7 +262,16 @@ const App: React.FC = () => { const [sourceInfo, setSourceInfo] = useState(); const [sourceConverted, setSourceConverted] = useState(false); const [renderAs, setRenderAs] = useState<'markdown' | 'html'>('markdown'); + // HTML plans render edge-to-edge (full-viewport) instead of in the centered, + // card-chromed markdown column. Branch the document-area containers on this. + const isHtmlSurface = renderAs === 'html'; const [rawHtml, setRawHtml] = useState(''); + // Session-level force-markdown preference (`--markdown`). When set, folder/linked HTML + // files are converted instead of rendered raw — threaded into /api/doc as &convert=1. + const [convertHtml, setConvertHtml] = useState(false); + // Hide the floating HTML annotation controls (toolstrip + action cluster) so the + // user can read the rendered page unobstructed. Selections/annotations are unaffected. + const [htmlToolsHidden, setHtmlToolsHidden] = useState(false); const [sourceFilePath, setSourceFilePath] = useState(); const [imageBaseDir, setImageBaseDir] = useState(undefined); const [isLoading, setIsLoading] = useState(true); @@ -301,6 +313,7 @@ const App: React.FC = () => { }; }); const [showPlanAIAnnouncement, setShowPlanAIAnnouncement] = useState(needsPlanAIAnnouncement); + const [showLookAndFeelAnnouncement, setShowLookAndFeelAnnouncement] = useState(needsLookAndFeelAnnouncement); const isMobile = useIsMobile(); const viewerRef = useRef(null); @@ -316,17 +329,28 @@ const App: React.FC = () => { usePrintMode(); + // Sidebar (shared TOC + Version Browser) + const sidebar = useSidebar(getUIPreferences().tocEnabled); + // Resizable panels - const panelResize = useResizablePanel({ storageKey: 'plannotator-panel-width' }); + const panelResize = useResizablePanel({ + storageKey: 'plannotator-panel-width', + // Drag the right panel skinny → snap it shut (matches the contents sidebar). + onSnapClose: () => setIsPanelOpen(false), + // Render-free drag: write the live width to a :root var the panel reads, + // so dragging never re-renders this (heavy) App. + apply: (w) => document.documentElement.style.setProperty('--rpanel-w', `${w}px`), + }); const tocResize = useResizablePanel({ storageKey: 'plannotator-toc-width', defaultWidth: 240, minWidth: 160, maxWidth: 400, side: 'left', + // Drag the contents panel skinny → snap it shut (prototype behavior). + onSnapClose: sidebar.close, + // Render-free drag: write the live width to a :root var the panel reads. + apply: (w) => document.documentElement.style.setProperty('--toc-w', `${w}px`), }); const isResizing = panelResize.isDragging || tocResize.isDragging; - // Sidebar (shared TOC + Version Browser) - const sidebar = useSidebar(getUIPreferences().tocEnabled); - // Whether the document has any TOC-eligible headings (level <= 3, matching // buildTocHierarchy). Drives the empty-doc auto-close behavior below — must // be declared before the effects that reference it (TDZ in dep arrays). @@ -395,6 +419,11 @@ const App: React.FC = () => { setShowPlanAIAnnouncement(false); }, []); + const dismissLookAndFeelAnnouncement = useCallback(() => { + markLookAndFeelAnnouncementSeen(); + setShowLookAndFeelAnnouncement(false); + }, []); + const handleAIChatToggle = useCallback(() => { dismissPlanAIAnnouncement(); if (wideModeType !== null) { @@ -406,7 +435,10 @@ const App: React.FC = () => { setIsPanelOpen(prev => rightSidebarTab === 'ai' ? !prev : true); }, [dismissPlanAIAnnouncement, exitWideMode, rightSidebarTab, wideModeType]); - // Sync sidebar open state when preference changes in Settings + // Sync sidebar open state when the "Auto-open Sidebar" preference changes in + // Settings. Deliberately does NOT react to the document or render mode — + // switching files (e.g. in annotate-folder) leaves the sidebar exactly as the + // user left it. useEffect(() => { if (wideModeType !== null) return; if (lastAppliedTocEnabledRef.current === uiPrefs.tocEnabled) return; @@ -466,6 +498,7 @@ const App: React.FC = () => { const linkedDocHook = useLinkedDoc({ markdown, annotations, selectedAnnotationId, globalAttachments, setMarkdown, setAnnotations, setSelectedAnnotationId, setGlobalAttachments, + renderAs, rawHtml, setRenderAs, setRawHtml, viewerRef, sidebar: linkedDocSidebar, sourceFilePath, sourceConverted, }); @@ -681,10 +714,10 @@ const App: React.FC = () => { const dirState = fileBrowser.dirs.find(d => d.path === dirPath); const buildUrl = dirState?.isVault ? (path: string) => `/api/reference/obsidian/doc?vaultPath=${encodeURIComponent(dirPath)}&path=${encodeURIComponent(path)}` - : (path: string) => `/api/doc?path=${encodeURIComponent(path)}&base=${encodeURIComponent(dirPath)}`; + : (path: string) => `/api/doc?path=${encodeURIComponent(path)}&base=${encodeURIComponent(dirPath)}${convertHtml ? '&convert=1' : ''}`; linkedDocHook.open(absolutePath, buildUrl, 'files'); fileBrowser.setActiveFile(absolutePath); - }, [linkedDocHook, fileBrowser]); + }, [linkedDocHook, fileBrowser, convertHtml]); // Route linked doc opens through the correct endpoint based on current context const handleOpenLinkedDoc = React.useCallback((docPath: string) => { @@ -697,7 +730,7 @@ const App: React.FC = () => { // When viewing a file browser doc, resolve links relative to current file's directory const baseDir = linkedDocHook.filepath?.replace(/\/[^/]+$/, '') || fileBrowser.activeDirPath; linkedDocHook.open(docPath, (path) => - `/api/doc?path=${encodeURIComponent(path)}&base=${encodeURIComponent(baseDir)}` + `/api/doc?path=${encodeURIComponent(path)}&base=${encodeURIComponent(baseDir)}${convertHtml ? '&convert=1' : ''}` ); } else { // Pass the current file's directory as base for relative path resolution @@ -706,13 +739,13 @@ const App: React.FC = () => { : imageBaseDir?.includes('/') ? imageBaseDir : undefined; if (baseDir) { linkedDocHook.open(docPath, (path) => - `/api/doc?path=${encodeURIComponent(path)}&base=${encodeURIComponent(baseDir)}` + `/api/doc?path=${encodeURIComponent(path)}&base=${encodeURIComponent(baseDir)}${convertHtml ? '&convert=1' : ''}` ); } else { linkedDocHook.open(docPath); } } - }, [fileBrowser.dirs, fileBrowser.activeDirPath, fileBrowser.activeFile, linkedDocHook, imageBaseDir]); + }, [fileBrowser.dirs, fileBrowser.activeDirPath, fileBrowser.activeFile, linkedDocHook, imageBaseDir, convertHtml]); // Wrap linked doc back to also clear file browser active file const handleLinkedDocBack = React.useCallback(() => { @@ -993,9 +1026,12 @@ const App: React.FC = () => { if (!res.ok) throw new Error('Not in API mode'); return res.json(); }) - .then((data: { plan: string; origin?: Origin; mode?: 'annotate' | 'annotate-last' | 'annotate-folder' | 'archive' | 'goal-setup'; goalSetup?: GoalSetupBundle; filePath?: string; sourceInfo?: string; sourceConverted?: boolean; gate?: boolean; renderAs?: 'html' | 'markdown'; rawHtml?: string; sharingEnabled?: boolean; shareBaseUrl?: string; pasteApiUrl?: string; repoInfo?: { display: string; branch?: string; host?: string }; previousPlan?: string | null; versionInfo?: { version: number; totalVersions: number; project: string }; archivePlans?: ArchivedPlan[]; projectRoot?: string; isWSL?: boolean; serverConfig?: { displayName?: string; gitUser?: string }; recentMessages?: PickerMessage[] }) => { + .then((data: { plan: string; origin?: Origin; mode?: 'annotate' | 'annotate-last' | 'annotate-folder' | 'archive' | 'goal-setup'; goalSetup?: GoalSetupBundle; filePath?: string; sourceInfo?: string; sourceConverted?: boolean; gate?: boolean; renderAs?: 'html' | 'markdown'; rawHtml?: string; convertHtml?: boolean; sharingEnabled?: boolean; shareBaseUrl?: string; pasteApiUrl?: string; repoInfo?: { display: string; branch?: string; host?: string }; previousPlan?: string | null; versionInfo?: { version: number; totalVersions: number; project: string }; archivePlans?: ArchivedPlan[]; projectRoot?: string; isWSL?: boolean; serverConfig?: { displayName?: string; gitUser?: string }; recentMessages?: PickerMessage[] }) => { // Initialize config store with server-provided values (config file > cookie > default) configStore.init(data.serverConfig); + // Session-level force-markdown preference (--markdown); threaded into folder/linked + // /api/doc requests so on-demand HTML files convert too. + setConvertHtml(data.convertHtml ?? false); setAISessionEnabled(data.mode !== 'archive' && data.mode !== 'goal-setup'); // gitUser drives the "Use git name" button in Settings; stays undefined (button hidden) when unavailable setGitUser(data.serverConfig?.gitUser); @@ -1730,7 +1766,9 @@ const App: React.FC = () => { const aiSourceConverted = linkedDocHook.isActive ? (linkedDocHook.getDocAnnotations().get(linkedDocHook.filepath ?? '')?.isConverted ?? false) : sourceConverted; - const aiRenderAs = linkedDocHook.isActive ? 'markdown' : renderAs; + // renderAs now tracks the active file (plan, linked doc, or folder file), so the AI + // sees the current surface's mode — raw HTML for an .html file, markdown otherwise. + const aiRenderAs = renderAs; const aiDocumentMode = annotateMode || linkedDocHook.isActive; const hasAIDocumentContext = !aiDocumentMode || @@ -2154,8 +2192,17 @@ const App: React.FC = () => { }, [uiPrefs.planWidth]); const annotateReaderMaxWidth = canUseWideMode && wideModeType === 'wide' ? null : planMaxWidth; const selectedAIProvider = aiProviders.find(provider => provider.id === aiConfig.providerId) ?? null; + // Only greet in a normal authoring context — not on a read-only shared session + // (a viewer would also be able to flip the owner's gridEnabled), nor over the + // goal-setup / permission-mode flows. Deferred (not marked seen) until then. + const shouldShowLookAndFeelAnnouncement = + showLookAndFeelAnnouncement && + !isSharedSession && + !goalSetupMode && + !showPermissionModeSetup; const shouldShowPlanAIAnnouncement = showPlanAIAnnouncement && + !shouldShowLookAndFeelAnnouncement && canUseAI && aiSessionEnabled && isApiMode && @@ -2179,6 +2226,9 @@ const App: React.FC = () => {
setHtmlToolsHidden((v) => !v)} isApiMode={isApiMode} annotateMode={annotateMode} archiveMode={archive.archiveMode} @@ -2271,13 +2321,13 @@ const App: React.FC = () => { showMessagesTab={annotateSource === 'message' && recentMessages.length > 1} hasMessageAnnotations={activeMessageAnnotationCounts.size > 0} hasFileAnnotations={hasFileAnnotations} - className="hidden lg:flex absolute left-0 top-0 z-10" + className="hidden lg:flex absolute left-0 top-0 z-20" /> )} {/* Left Sidebar: open state (TOC or Version Browser) */} {sidebar.isOpen && !goalSetupMode && ( - <> +
{ @@ -2285,7 +2335,7 @@ const App: React.FC = () => { if (tab === 'archive' && !archive.archiveMode) archive.fetchPlans(); }} onClose={sidebar.close} - width={tocResize.width} + width={`var(--toc-w, ${tocResize.width}px)`} blocks={blocks} annotations={annotations} activeSection={activeSection} @@ -2324,14 +2374,14 @@ const App: React.FC = () => { onSelectMessage={handleSelectMessage} messageAnnotationCounts={activeMessageAnnotationCounts} /> - - + +
)} {/* Document Area */} @@ -2345,7 +2395,7 @@ const App: React.FC = () => { cancelText="Dismiss" showCancel /> -
+
{/* Sticky header lane — ghost bar that pins the toolstrip + badges at top: 12px once the user scrolls. Invisible at top of doc; original toolstrip/badges remain the source of @@ -2353,7 +2403,7 @@ const App: React.FC = () => { sticky actions are disabled. remountToken re-anchors the ResizeObserver when Viewer swaps content (linked docs or message switches). */} - {!goalSetupMode && !isPlanDiffActive && !archive.archiveMode && uiPrefs.stickyActionsEnabled && ( + {!goalSetupMode && !isPlanDiffActive && !isHtmlSurface && !archive.archiveMode && uiPrefs.stickyActionsEnabled && ( { /> )} - {/* Annotation Toolstrip (hidden during plan diff and archive mode) */} - {!goalSetupMode && !isPlanDiffActive && !archive.archiveMode && ( -
+ {/* Annotation Toolstrip — the mode switcher (selection/redline input + + comment/markup mode). Hidden during plan diff, and on HTML surfaces + when the header's "Hide tools" toggle is on (leaving the rendered HTML + free of overlay controls). On HTML it floats top-left over the doc. */} + {!goalSetupMode && !isPlanDiffActive && !archive.archiveMode && !(isHtmlSurface && htmlToolsHidden) && ( +
)} @@ -2422,13 +2482,13 @@ const App: React.FC = () => {

Select a file to annotate

-

Pick a markdown file from the sidebar to begin.

+

Pick a markdown or HTML file from the sidebar to begin.

)} {/* Normal Plan View — always mounted, hidden during diff mode */} -
- {canUseWideMode && !isPlanDiffActive && !archive.archiveMode && ( +
+ {canUseWideMode && !isPlanDiffActive && !archive.archiveMode && !isHtmlSurface && (
{ )} {renderAs === 'html' ? ( { onSelectAnnotation={handleSelectAnnotation} selectedAnnotationId={selectedAnnotationId} mode={editorMode} + inputMethod={inputMethod} globalAttachments={globalAttachments} onAddGlobalAttachment={handleAddGlobalAttachment} onRemoveGlobalAttachment={handleRemoveGlobalAttachment} - maxWidth={annotateReaderMaxWidth} + maxWidth={isHtmlSurface ? null : annotateReaderMaxWidth} + fullViewport={isHtmlSurface} + hideControls={htmlToolsHidden} onAskAI={canUseAI ? handleAskAI : undefined} /> ) : ( @@ -2490,6 +2554,7 @@ const App: React.FC = () => { mode={editorMode} inputMethod={inputMethod} taterMode={taterMode} + gridEnabled={gridEnabled} globalAttachments={globalAttachments} onAddGlobalAttachment={handleAddGlobalAttachment} onRemoveGlobalAttachment={handleRemoveGlobalAttachment} @@ -2530,8 +2595,13 @@ const App: React.FC = () => {
+ {/* Right panel region — `group/sidebar` so the collapse button reveals when + hovering the whole panel, not just the thin handle. The handle and the + panel(s) are separate sibling conditionals, so they need a shared hover + ancestor (`contents` = no layout box). */} +
{/* Resize Handle */} - {isPanelOpen && wideModeType === null && !goalSetupMode && (rightSidebarTab === 'annotations' || canUseAI) && } + {isPanelOpen && wideModeType === null && !goalSetupMode && (rightSidebarTab === 'annotations' || canUseAI) && setIsPanelOpen(false)} />} {/* Annotation Panel */} { onDeleteCodeAnnotation={handleDeleteCodeAnnotation} onEditCodeAnnotation={handleEditCodeAnnotation} sharingEnabled={canShareCurrentSession} - width={panelResize.width} + width={`var(--rpanel-w, ${panelResize.width}px)`} editorAnnotations={editorAnnotations} onDeleteEditorAnnotation={deleteEditorAnnotation} onClose={() => setIsPanelOpen(false)} @@ -2562,31 +2632,35 @@ const App: React.FC = () => { {isPanelOpen && rightSidebarTab === 'ai' && wideModeType === null && !goalSetupMode && canUseAI && ( )} +
@@ -2790,6 +2865,13 @@ const App: React.FC = () => { onDismiss={dismissPlanAIAnnouncement} /> + configStore.set('gridEnabled', v)} + onDismiss={dismissLookAndFeelAnnouncement} + /> + {/* Image Annotator for pasted images */} void; // Mode flags (stable after mount) isApiMode: boolean; annotateMode: boolean; @@ -86,6 +91,9 @@ interface AppHeaderProps { } export const AppHeader = React.memo(({ + htmlSurface, + htmlToolsHidden, + onToggleHtmlTools, isApiMode, annotateMode, archiveMode, @@ -150,7 +158,19 @@ export const AppHeader = React.memo(({ }) => { return (
- +
+ + {htmlSurface && onToggleHtmlTools && ( + + )} +
{/* Bot callback buttons — only shown when ?cb=&ct= params are present */} diff --git a/packages/editor/index.css b/packages/editor/index.css index fc236c04..6e5ab535 100644 --- a/packages/editor/index.css +++ b/packages/editor/index.css @@ -1,5 +1,9 @@ +@import "@fontsource-variable/inter"; +@import "@fontsource-variable/geist-mono"; @import "tailwindcss"; +@plugin "tailwindcss-animate"; + /* Tell Tailwind where to scan for classes */ @source "../ui/components/**/*.tsx"; @source "../ui/hooks/**/*.ts"; diff --git a/packages/editor/package.json b/packages/editor/package.json index f5d961b6..8d9f83e1 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -8,12 +8,15 @@ "./shortcuts": "./shortcuts.ts" }, "dependencies": { + "@fontsource-variable/geist-mono": "5.2.7", + "@fontsource-variable/inter": "^5.2.8", "@plannotator/ai": "workspace:*", "@plannotator/shared": "workspace:*", "@plannotator/ui": "workspace:*", "react": "^19.2.3", "react-dom": "^19.2.3", "sonner": "^2.0.7", - "tailwindcss": "^4.1.18" + "tailwindcss": "^4.1.18", + "tailwindcss-animate": "^1.0.7" } } diff --git a/packages/review-editor/App.tsx b/packages/review-editor/App.tsx index 8ef69c55..c7c87337 100644 --- a/packages/review-editor/App.tsx +++ b/packages/review-editor/App.tsx @@ -40,6 +40,7 @@ import { useExternalAnnotations } from '@plannotator/ui/hooks/useExternalAnnotat import { useAgentJobs } from '@plannotator/ui/hooks/useAgentJobs'; import { exportEditorAnnotations } from '@plannotator/ui/utils/parser'; import { ResizeHandle } from '@plannotator/ui/components/ResizeHandle'; +import { FolderTree } from 'lucide-react'; import { DockviewReact, type DockviewReadyEvent, type DockviewApi } from 'dockview-react'; import { ReviewHeaderMenu } from './components/ReviewHeaderMenu'; import { ReviewSidebar } from './components/ReviewSidebar'; @@ -575,10 +576,14 @@ const ReviewApp: React.FC = () => { }, [askAI]); // Resizable panels - const panelResize = useResizablePanel({ storageKey: 'plannotator-review-panel-width' }); + const panelResize = useResizablePanel({ + storageKey: 'plannotator-review-panel-width', + onSnapClose: () => reviewSidebar.close(), + }); const fileTreeResize = useResizablePanel({ storageKey: 'plannotator-filetree-width', defaultWidth: 256, minWidth: 160, maxWidth: 400, side: 'left', + onSnapClose: () => setIsFileTreeOpen(false), }); const isResizing = panelResize.isDragging || fileTreeResize.isDragging; @@ -1815,9 +1820,7 @@ const ReviewApp: React.FC = () => { }`} title={isFileTreeOpen ? 'Hide file tree' : 'Show file tree'} > - - - +
@@ -2080,21 +2083,6 @@ const ReviewApp: React.FC = () => {
- setOpenSettingsMenu(true)} - onOpenExport={() => setShowExportModal(true)} - onToggleFileTree={() => setIsFileTreeOpen(prev => !prev)} - onToggleSidebar={() => reviewSidebar.isOpen ? reviewSidebar.close() : reviewSidebar.open()} - isFileTreeOpen={isFileTreeOpen} - isSidebarOpen={reviewSidebar.isOpen} - appVersion={appVersion} - updateInfo={updateInfo} - origin={origin} - isWSL={isWSL} - /> - -
- {/* Sidebar tab toggles */} )} + +
+ + setOpenSettingsMenu(true)} + onOpenExport={() => setShowExportModal(true)} + onToggleFileTree={() => setIsFileTreeOpen(prev => !prev)} + onToggleSidebar={() => reviewSidebar.isOpen ? reviewSidebar.close() : reviewSidebar.open()} + isFileTreeOpen={isFileTreeOpen} + isSidebarOpen={reviewSidebar.isOpen} + appVersion={appVersion} + updateInfo={updateInfo} + origin={origin} + isWSL={isWSL} + />
{/* Main content */}
{shouldShowFileTree && isFileTreeOpen && ( - <> +
{ onStepSearchMatch={hasSearchableFiles ? stepSearchMatch : undefined} repoRoot={prMetadata ? null : (activeWorktreePath ?? agentCwd ?? gitContext?.cwd ?? null)} /> - - + setIsFileTreeOpen(false)} /> +
)} {/* Center dock area */} @@ -2288,8 +2291,8 @@ const ReviewApp: React.FC = () => { {/* Resize Handle + Sidebar */} {reviewSidebar.isOpen && ( - <> - +
+ reviewSidebar.close()} /> { onOpenJobDetail={handleOpenJobDetail} onOpenPRPanel={handleOpenPRPanel} /> - +
)}
diff --git a/packages/review-editor/components/FileTreeNode.tsx b/packages/review-editor/components/FileTreeNode.tsx index 046c8d74..c6c12d04 100644 --- a/packages/review-editor/components/FileTreeNode.tsx +++ b/packages/review-editor/components/FileTreeNode.tsx @@ -79,8 +79,12 @@ export const FileTreeNodeItem: React.FC = ({ {node.name} {(node.additions > 0 || node.deletions > 0) && (
- +{node.additions} - -{node.deletions} + {node.additions > 0 && ( + +{node.additions} + )} + {node.deletions > 0 && ( + -{node.deletions} + )}
)} @@ -155,8 +159,12 @@ export const FileTreeNodeItem: React.FC = ({ {annotationCount > 0 && ( {annotationCount} )} - +{node.file!.additions} - -{node.file!.deletions} + {node.file!.additions > 0 && ( + +{node.file!.additions} + )} + {node.file!.deletions > 0 && ( + -{node.file!.deletions} + )}
diff --git a/packages/review-editor/components/PRSelector.tsx b/packages/review-editor/components/PRSelector.tsx index baab535b..3e5841de 100644 --- a/packages/review-editor/components/PRSelector.tsx +++ b/packages/review-editor/components/PRSelector.tsx @@ -8,7 +8,7 @@ type PRItem = PRListItem; const stateColors: Record = { open: 'text-success', - merged: 'text-accent', + merged: 'text-annotation-comment', closed: 'text-muted-foreground/60', }; @@ -112,7 +112,7 @@ export function PRSelector({ mrNumberLabel, prTitle, currentNumber, onSelect, di

{activeTab === 'annotations' ? 'Annotations' : activeTab === 'ai' ? 'AI' : 'Review Agents'}

@@ -375,6 +366,7 @@ export const ReviewSidebar: React.FC = /* React.memo */({ onDeleteEditorAnnotation?.(ann.id)} /> ))} diff --git a/packages/review-editor/components/StackedPRLabel.tsx b/packages/review-editor/components/StackedPRLabel.tsx index a953c821..a40caf10 100644 --- a/packages/review-editor/components/StackedPRLabel.tsx +++ b/packages/review-editor/components/StackedPRLabel.tsx @@ -151,7 +151,7 @@ export function StackedPRLabel({ type="button" disabled={isSwitchingScope} title={`Stack: comparing vs ${scopeTarget}`} - className="text-[10px] text-accent/70 hover:text-accent inline-flex items-center gap-1 whitespace-nowrap transition-colors rounded px-1.5 py-0.5 hover:bg-muted/20 disabled:opacity-60 disabled:cursor-wait" + className="text-[10px] text-annotation-comment/70 hover:text-annotation-comment inline-flex items-center gap-1 whitespace-nowrap transition-colors rounded px-1.5 py-0.5 hover:bg-muted/20 disabled:opacity-60 disabled:cursor-wait" > @@ -223,7 +223,7 @@ export function StackedPRLabel({ )}
+ ))} +
+ ); +} + +// Animated on/off switch (fast mode). +function Toggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) { + return ( + + ); +} + +// Dropdown button + downward popover. Reused for the provider selector and the +// model picker (whose 7–9 options rule out a segmented control). The popover +// opens downward (`top-full`) because the launch panel is pinned to the top of +// the tab. +function SelectMenu({ value, options, onChange, icon, placeholder }: { value: string; options: Array<{ value: string; label: string }>; onChange: (v: string) => void; icon?: React.ReactNode; placeholder?: string }) { + const [open, setOpen] = useState(false); + const current = options.find((o) => o.value === value); + return ( +
+ + {open && ( + <> +
setOpen(false)} /> +
+ {options.map((o) => ( + + ))} +
+ + )} +
); } @@ -213,57 +291,62 @@ function JobCard({ return (
onViewDetails() : (isTerminal ? onToggle : undefined)} > -
-
- - {job.label} -
-
- {annotationCount > 0 && ( - - {annotationCount} +
+ +
+
+ {job.label} + {onViewDetails && } +
+
+ {job.provider} + {job.model && ( + {formatModel(job.provider, job.engine, job.model)} + )} + {job.effort && {formatEffort(job.effort)}} + {job.reasoningEffort && {formatReasoning(job.reasoningEffort)}} + {job.fastMode && ( + + fast + + )} + · + + {isTerminal && job.endedAt ? formatDuration(job.endedAt - job.startedAt) : } - )} - - {isTerminal && job.endedAt - ? formatDuration(job.endedAt - job.startedAt) - : - } - + {annotationCount > 0 && ( + <> + · + {annotationCount} finding{annotationCount !== 1 ? 's' : ''} + + )} +
-
- -
- {!isTerminal && ( - - )} -
-
+ {!isTerminal && ( + + )} - {/* Error details — fallback for when dockview detail panel is not available */} + {/* Error details — fallback for when the dockview detail panel is not available */} {!onViewDetails && job.status === 'failed' && job.error && expanded && ( -
-
+        
+
             {job.error}
           
@@ -390,203 +473,117 @@ export const AgentsTab: React.FC = ({ return (
- {/* Launch bar */} + {/* Launch panel (pinned to the top) */} {availableProviders.length > 0 && ( -
-
- {availableProviders.length > 1 ? ( - - ) : ( - - {availableProviders[0] ? providerDropdownLabel(availableProviders[0].id, availableProviders[0].name) : ''} - - )} - +
+
+ Launch agent
- {/* Claude model + effort config */} - {selectedProvider === 'claude' && ( -
-
- Model - +
+ {/* Provider */} + {availableProviders.length > 1 ? ( + ({ value: p.id, label: providerDropdownLabel(p.id, p.name) }))} + onChange={setSelectedProvider} + icon={} + placeholder="Select provider" + /> + ) : ( +
+ + + {availableProviders[0] ? providerDropdownLabel(availableProviders[0].id, availableProviders[0].name) : ''} +
-
- Effort - -
-
- )} + )} - {/* Codex model + reasoning + fast mode config */} - {selectedProvider === 'codex' && ( -
-
- Model - -
-
- Reasoning - -
-
- Fast - -
-
- )} + + {tourEngine === 'claude' && ( + + + + )} + {tourEngine === 'codex' && ( + <> + + + + + + + + )} + + )} +
- {/* Tour engine/model config — only shown when tour is selected */} - {selectedProvider === 'tour' && ( -
- {/* Engine selector */} - {claudeAvailable && codexAvailable && ( -
- Engine - - -
- )} - - {/* Model selector — engine-specific options */} -
- Model - -
- - {/* Claude-only: effort level */} - {tourEngine === 'claude' && ( -
- Effort - -
- )} - - {/* Codex-only: reasoning effort + fast mode */} - {tourEngine === 'codex' && ( - <> -
- Reasoning - -
-
- Fast - -
- - )} -
- )} +
)} - {/* Job list */} + {/* Job list (scrolls; launch controls are pinned above) */}
{sortedJobs.length === 0 ? ( -
-
- +
+
+
-

- No agent jobs yet -

-

- Launch an agent to get automated review findings -

+

No agent jobs

+

Launch an agent above

) : ( sortedJobs.map((job) => ( @@ -603,17 +600,15 @@ export const AgentsTab: React.FC = ({ )}
- {/* Kill All footer */} + {/* Kill all — pinned at the bottom */} {runningCount >= 2 && ( -
+
)} diff --git a/packages/ui/components/AnnotationPanel.tsx b/packages/ui/components/AnnotationPanel.tsx index 2d8922c8..c2163a6e 100644 --- a/packages/ui/components/AnnotationPanel.tsx +++ b/packages/ui/components/AnnotationPanel.tsx @@ -5,6 +5,38 @@ import { ImageThumbnail } from './ImageThumbnail'; import { EditorAnnotationCard } from './EditorAnnotationCard'; import { useIsMobile } from '../hooks/useIsMobile'; import { OverlayScrollArea } from './OverlayScrollArea'; +import { Button } from './ui/button'; +import { cn } from '../lib/utils'; + +// Card type-word colors. Deletion uses `destructive` (reliably red on every +// theme, matching the in-document .deletion highlight). Comment uses the +// `annotation-comment` token, which defaults to each theme's --accent (so it +// stays consistent with the .comment highlight) but is overridden to a legible +// blue in neutral themes whose accent is a low-contrast gray (e.g. "simple"). +// Global has no in-document highlight, so it uses a fixed legible purple. +const TYPE_COLOR: Record = { + [AnnotationType.DELETION]: 'text-destructive', + [AnnotationType.COMMENT]: 'text-annotation-comment', + [AnnotationType.GLOBAL_COMMENT]: 'text-purple-500', +}; + +const TYPE_LABEL: Record = { + [AnnotationType.DELETION]: 'Deletion', + [AnnotationType.COMMENT]: 'Comment', + [AnnotationType.GLOBAL_COMMENT]: 'Global', +}; + +const PencilIcon = () => ( + + + +); + +const TrashCardIcon = () => ( + + + +); interface PanelProps { isOpen: boolean; @@ -19,7 +51,7 @@ interface PanelProps { onDeleteCodeAnnotation?: (id: string) => void; onEditCodeAnnotation?: (id: string, updates: Partial) => void; sharingEnabled?: boolean; - width?: number; + width?: number | string; editorAnnotations?: EditorAnnotation[]; onDeleteEditorAnnotation?: (id: string) => void; onClose?: () => void; @@ -76,38 +108,42 @@ export const AnnotationPanel: React.FC = ({ const panel = (