@@ -13,7 +13,9 @@ import { AnnotationToolstrip } from '@plannotator/ui/components/AnnotationToolst
1313import { TaterSpriteRunning } from '@plannotator/ui/components/TaterSpriteRunning' ;
1414import { TaterSpritePullup } from '@plannotator/ui/components/TaterSpritePullup' ;
1515import { Settings } from '@plannotator/ui/components/Settings' ;
16+ import { FeedbackButton , ApproveButton } from '@plannotator/ui/components/ToolbarButtons' ;
1617import { useSharing } from '@plannotator/ui/hooks/useSharing' ;
18+ import { getCallbackConfig , CallbackAction , executeCallback , type ToastPayload } from '@plannotator/ui/utils/callback' ;
1719import { useAgents } from '@plannotator/ui/hooks/useAgents' ;
1820import { useActiveSection } from '@plannotator/ui/hooks/useActiveSection' ;
1921import { storage } from '@plannotator/ui/utils/storage' ;
@@ -115,7 +117,7 @@ const App: React.FC = () => {
115117
116118 const [ showExportDropdown , setShowExportDropdown ] = useState ( false ) ;
117119 const [ initialExportTab , setInitialExportTab ] = useState < 'share' | 'annotations' | 'notes' > ( ) ;
118- const [ noteSaveToast , setNoteSaveToast ] = useState < { type : 'success' | 'error' ; message : string } | null > ( null ) ;
120+ const [ noteSaveToast , setNoteSaveToast ] = useState < ToastPayload > ( null ) ;
119121 const [ isPlanDiffActive , setIsPlanDiffActive ] = useState ( false ) ;
120122 const [ planDiffMode , setPlanDiffMode ] = useState < PlanDiffMode > ( 'clean' ) ;
121123 const [ previousPlan , setPreviousPlan ] = useState < string | null > ( null ) ;
@@ -924,6 +926,29 @@ const App: React.FC = () => {
924926 return output ;
925927 } , [ blocks , allAnnotations , globalAttachments , linkedDocHook . getDocAnnotations , editorAnnotations ] ) ;
926928
929+ // Bot callback config — read once from URL search params (?cb=&ct=)
930+ const callbackConfig = React . useMemo ( ( ) => getCallbackConfig ( ) , [ ] ) ;
931+
932+ const callCallback = React . useCallback ( async ( action : CallbackAction ) => {
933+ if ( ! callbackConfig || isSubmitting || ! shareUrl ) return ;
934+ setIsSubmitting ( true ) ;
935+ try {
936+ const toast = await executeCallback ( action , callbackConfig , shareUrl ) ;
937+ if ( toast ) {
938+ setNoteSaveToast ( toast ) ;
939+ setTimeout ( ( ) => setNoteSaveToast ( null ) , 4000 ) ;
940+ if ( toast . type === 'success' ) {
941+ setSubmitted ( action === CallbackAction . Approve ? 'approved' : 'denied' ) ;
942+ }
943+ }
944+ } finally {
945+ setIsSubmitting ( false ) ;
946+ }
947+ } , [ callbackConfig , isSubmitting , shareUrl ] ) ;
948+
949+ const handleCallbackApprove = React . useCallback ( ( ) => callCallback ( CallbackAction . Approve ) , [ callCallback ] ) ;
950+ const handleCallbackFeedback = React . useCallback ( ( ) => callCallback ( CallbackAction . Feedback ) , [ callCallback ] ) ;
951+
927952 // Quick-save handlers for export dropdown and keyboard shortcut
928953 const handleDownloadAnnotations = ( ) => {
929954 setShowExportDropdown ( false ) ;
@@ -1109,6 +1134,25 @@ const App: React.FC = () => {
11091134 </ div >
11101135
11111136 < div className = "flex items-center gap-1 md:gap-2" >
1137+ { /* Bot callback buttons — only shown when ?cb=&ct= params are present */ }
1138+ { callbackConfig && ! isApiMode && isSharedSession && (
1139+ < >
1140+ < div className = "w-px h-5 bg-border/50 mx-1 hidden md:block" />
1141+ < FeedbackButton
1142+ onClick = { handleCallbackFeedback }
1143+ disabled = { isSubmitting || ! shareUrl }
1144+ isLoading = { isSubmitting }
1145+ title = "Send feedback to bot"
1146+ />
1147+ < ApproveButton
1148+ onClick = { handleCallbackApprove }
1149+ disabled = { isSubmitting || ! shareUrl }
1150+ isLoading = { isSubmitting }
1151+ title = "Approve design and notify bot"
1152+ />
1153+ </ >
1154+ ) }
1155+
11121156 { isApiMode && ! linkedDocHook . isActive && archive . archiveMode && (
11131157 < >
11141158 < button
@@ -1133,7 +1177,7 @@ const App: React.FC = () => {
11331177
11341178 { isApiMode && ! linkedDocHook . isActive && ! archive . archiveMode && (
11351179 < >
1136- < button
1180+ < FeedbackButton
11371181 onClick = { ( ) => {
11381182 if ( annotateMode ) {
11391183 handleAnnotateFeedback ( ) ;
@@ -1150,29 +1194,18 @@ const App: React.FC = () => {
11501194 }
11511195 } }
11521196 disabled = { isSubmitting }
1153- className = { `p-1.5 md:px-2.5 md:py-1 rounded-md text-xs font-medium transition-all ${
1154- isSubmitting
1155- ? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
1156- : 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/30'
1157- } `}
1197+ isLoading = { isSubmitting }
1198+ label = { annotateMode ? ( allAnnotations . length > 0 || editorAnnotations . length > 0 || linkedDocHook . docAnnotationCount > 0 ? 'Send Annotations' : 'Done' ) : 'Send Feedback' }
11581199 title = { annotateMode ? ( allAnnotations . length > 0 || editorAnnotations . length > 0 || linkedDocHook . docAnnotationCount > 0 ? 'Send Annotations' : 'Done' ) : 'Send Feedback' }
1159- >
1160- < svg className = "w-4 h-4 md:hidden" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
1161- < path strokeLinecap = "round" strokeLinejoin = "round" d = "M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
1162- </ svg >
1163- < span className = "hidden md:inline" > { isSubmitting ? 'Sending...' : annotateMode ? ( allAnnotations . length > 0 || editorAnnotations . length > 0 || linkedDocHook . docAnnotationCount > 0 ? 'Send Annotations' : 'Done' ) : 'Send Feedback' } </ span >
1164- </ button >
1200+ />
11651201
11661202 { ! annotateMode && < div className = "relative group/approve" >
1167- < button
1203+ < ApproveButton
11681204 onClick = { ( ) => {
1169- // Show warning for Claude Code users with annotations
11701205 if ( origin === 'claude-code' && allAnnotations . length > 0 ) {
11711206 setShowClaudeCodeWarning ( true ) ;
11721207 return ;
11731208 }
1174-
1175- // Check if agent exists for OpenCode users
11761209 if ( origin === 'opencode' ) {
11771210 const warning = getAgentWarning ( ) ;
11781211 if ( warning ) {
@@ -1181,21 +1214,12 @@ const App: React.FC = () => {
11811214 return ;
11821215 }
11831216 }
1184-
11851217 handleApprove ( ) ;
11861218 } }
11871219 disabled = { isSubmitting }
1188- className = { `px-2 py-1 md:px-2.5 rounded-md text-xs font-medium transition-all ${
1189- isSubmitting
1190- ? 'opacity-50 cursor-not-allowed bg-muted text-muted-foreground'
1191- : origin === 'claude-code' && allAnnotations . length > 0
1192- ? 'bg-success/50 text-success-foreground/70 hover:bg-success hover:text-success-foreground'
1193- : 'bg-success text-success-foreground hover:opacity-90'
1194- } `}
1195- >
1196- < span className = "md:hidden" > { isSubmitting ? '...' : 'OK' } </ span >
1197- < span className = "hidden md:inline" > { isSubmitting ? 'Approving...' : 'Approve' } </ span >
1198- </ button >
1220+ isLoading = { isSubmitting }
1221+ dimmed = { origin === 'claude-code' && allAnnotations . length > 0 }
1222+ />
11991223 { origin === 'claude-code' && allAnnotations . length > 0 && (
12001224 < div className = "absolute top-full right-0 mt-2 px-3 py-2 bg-popover border border-border rounded-lg shadow-xl text-xs text-foreground w-56 text-center opacity-0 invisible group-hover/approve:opacity-100 group-hover/approve:visible transition-all pointer-events-none z-50" >
12011225 < div className = "absolute bottom-full right-4 border-4 border-transparent border-b-border" />
0 commit comments