@@ -20,6 +20,7 @@ import { CompletionOverlay } from '@plannotator/ui/components/CompletionOverlay'
2020import { UpdateBanner } from '@plannotator/ui/components/UpdateBanner' ;
2121import { getObsidianSettings , getEffectiveVaultPath , isObsidianConfigured , CUSTOM_PATH_SENTINEL } from '@plannotator/ui/utils/obsidian' ;
2222import { getBearSettings } from '@plannotator/ui/utils/bear' ;
23+ import { getOctarineSettings , isOctarineConfigured } from '@plannotator/ui/utils/octarine' ;
2324import { getDefaultNotesApp } from '@plannotator/ui/utils/defaultNotesApp' ;
2425import { getAgentSwitchSettings , getEffectiveAgentName } from '@plannotator/ui/utils/agentSwitch' ;
2526import { getPlanSaveSettings } from '@plannotator/ui/utils/planSave' ;
@@ -366,29 +367,52 @@ const App: React.FC = () => {
366367 setBlocks ( parseMarkdownToBlocks ( markdown ) ) ;
367368 } , [ markdown ] ) ;
368369
369- // Auto-save to Obsidian on plan arrival (if enabled )
370+ // Auto-save to notes apps on plan arrival (each gated by its autoSave toggle )
370371 const autoSaveAttempted = useRef ( false ) ;
371372 useEffect ( ( ) => {
372373 if ( ! isApiMode || ! markdown || isSharedSession || annotateMode ) return ;
373374 if ( autoSaveAttempted . current ) return ;
374375
375- const obsSettings = getObsidianSettings ( ) ;
376- if ( ! obsSettings . autoSave || ! obsSettings . enabled ) return ;
376+ const body : { obsidian ?: object ; bear ?: object ; octarine ?: object } = { } ;
377+ const targets : string [ ] = [ ] ;
377378
378- const vaultPath = getEffectiveVaultPath ( obsSettings ) ;
379- if ( ! vaultPath ) return ;
379+ const obsSettings = getObsidianSettings ( ) ;
380+ if ( obsSettings . autoSave && obsSettings . enabled ) {
381+ const vaultPath = getEffectiveVaultPath ( obsSettings ) ;
382+ if ( vaultPath ) {
383+ body . obsidian = {
384+ vaultPath,
385+ folder : obsSettings . folder || 'plannotator' ,
386+ plan : markdown ,
387+ ...( obsSettings . filenameFormat && { filenameFormat : obsSettings . filenameFormat } ) ,
388+ ...( obsSettings . filenameSeparator && obsSettings . filenameSeparator !== 'space' && { filenameSeparator : obsSettings . filenameSeparator } ) ,
389+ } ;
390+ targets . push ( 'Obsidian' ) ;
391+ }
392+ }
380393
381- autoSaveAttempted . current = true ;
394+ const bearSettings = getBearSettings ( ) ;
395+ if ( bearSettings . autoSave && bearSettings . enabled ) {
396+ body . bear = {
397+ plan : markdown ,
398+ customTags : bearSettings . customTags ,
399+ tagPosition : bearSettings . tagPosition ,
400+ } ;
401+ targets . push ( 'Bear' ) ;
402+ }
382403
383- const body = {
384- obsidian : {
385- vaultPath,
386- folder : obsSettings . folder || 'plannotator' ,
404+ const octSettings = getOctarineSettings ( ) ;
405+ if ( octSettings . autoSave && octSettings . enabled && octSettings . workspace ) {
406+ body . octarine = {
387407 plan : markdown ,
388- ...( obsSettings . filenameFormat && { filenameFormat : obsSettings . filenameFormat } ) ,
389- ...( obsSettings . filenameSeparator && obsSettings . filenameSeparator !== 'space' && { filenameSeparator : obsSettings . filenameSeparator } ) ,
390- } ,
391- } ;
408+ workspace : octSettings . workspace ,
409+ folder : octSettings . folder || 'plannotator' ,
410+ } ;
411+ targets . push ( 'Octarine' ) ;
412+ }
413+
414+ if ( targets . length === 0 ) return ;
415+ autoSaveAttempted . current = true ;
392416
393417 fetch ( '/api/save-notes' , {
394418 method : 'POST' ,
@@ -397,14 +421,15 @@ const App: React.FC = () => {
397421 } )
398422 . then ( res => res . json ( ) )
399423 . then ( data => {
400- if ( data . results ?. obsidian ?. success ) {
401- setNoteSaveToast ( { type : 'success' , message : 'Auto-saved to Obsidian' } ) ;
424+ const failed = targets . filter ( t => ! data . results ?. [ t . toLowerCase ( ) ] ?. success ) ;
425+ if ( failed . length === 0 ) {
426+ setNoteSaveToast ( { type : 'success' , message : `Auto-saved to ${ targets . join ( ' & ' ) } ` } ) ;
402427 } else {
403- setNoteSaveToast ( { type : 'error' , message : ' Auto-save to Obsidian failed' } ) ;
428+ setNoteSaveToast ( { type : 'error' , message : ` Auto-save failed for ${ failed . join ( ' & ' ) } ` } ) ;
404429 }
405430 } )
406431 . catch ( ( ) => {
407- setNoteSaveToast ( { type : 'error' , message : 'Auto-save to Obsidian failed' } ) ;
432+ setNoteSaveToast ( { type : 'error' , message : 'Auto-save failed' } ) ;
408433 } )
409434 . finally ( ( ) => setTimeout ( ( ) => setNoteSaveToast ( null ) , 3000 ) ) ;
410435 } , [ isApiMode , markdown , isSharedSession , annotateMode ] ) ;
@@ -471,11 +496,12 @@ const App: React.FC = () => {
471496 try {
472497 const obsidianSettings = getObsidianSettings ( ) ;
473498 const bearSettings = getBearSettings ( ) ;
499+ const octarineSettings = getOctarineSettings ( ) ;
474500 const agentSwitchSettings = getAgentSwitchSettings ( ) ;
475501 const planSaveSettings = getPlanSaveSettings ( ) ;
476502
477503 // Build request body - include integrations if enabled
478- const body : { obsidian ?: object ; bear ?: object ; feedback ?: string ; agentSwitch ?: string ; planSave ?: { enabled : boolean ; customPath ?: string } ; permissionMode ?: string } = { } ;
504+ const body : { obsidian ?: object ; bear ?: object ; octarine ?: object ; feedback ?: string ; agentSwitch ?: string ; planSave ?: { enabled : boolean ; customPath ?: string } ; permissionMode ?: string } = { } ;
479505
480506 // Include permission mode for Claude Code
481507 if ( origin === 'claude-code' ) {
@@ -513,6 +539,14 @@ const App: React.FC = () => {
513539 } ;
514540 }
515541
542+ if ( octarineSettings . enabled && octarineSettings . workspace ) {
543+ body . octarine = {
544+ plan : markdown ,
545+ workspace : octarineSettings . workspace ,
546+ folder : octarineSettings . folder || 'plannotator' ,
547+ } ;
548+ }
549+
516550 // Include annotations as feedback if any exist (for OpenCode "approve with notes")
517551 const hasDocAnnotations = Array . from ( linkedDocHook . getDocAnnotations ( ) . values ( ) ) . some (
518552 ( d ) => d . annotations . length > 0 || d . globalAttachments . length > 0
@@ -717,9 +751,9 @@ const App: React.FC = () => {
717751 setTimeout ( ( ) => setNoteSaveToast ( null ) , 3000 ) ;
718752 } ;
719753
720- const handleQuickSaveToNotes = async ( target : 'obsidian' | 'bear' ) => {
754+ const handleQuickSaveToNotes = async ( target : 'obsidian' | 'bear' | 'octarine' ) => {
721755 setShowExportDropdown ( false ) ;
722- const body : { obsidian ?: object ; bear ?: object } = { } ;
756+ const body : { obsidian ?: object ; bear ?: object ; octarine ?: object } = { } ;
723757
724758 if ( target === 'obsidian' ) {
725759 const s = getObsidianSettings ( ) ;
@@ -742,7 +776,16 @@ const App: React.FC = () => {
742776 tagPosition : bs . tagPosition ,
743777 } ;
744778 }
779+ if ( target === 'octarine' ) {
780+ const os = getOctarineSettings ( ) ;
781+ body . octarine = {
782+ plan : markdown ,
783+ workspace : os . workspace ,
784+ folder : os . folder || 'plannotator' ,
785+ } ;
786+ }
745787
788+ const targetName = target === 'obsidian' ? 'Obsidian' : target === 'bear' ? 'Bear' : 'Octarine' ;
746789 try {
747790 const res = await fetch ( '/api/save-notes' , {
748791 method : 'POST' ,
@@ -752,7 +795,7 @@ const App: React.FC = () => {
752795 const data = await res . json ( ) ;
753796 const result = data . results ?. [ target ] ;
754797 if ( result ?. success ) {
755- setNoteSaveToast ( { type : 'success' , message : `Saved to ${ target === 'obsidian' ? 'Obsidian' : 'Bear' } ` } ) ;
798+ setNoteSaveToast ( { type : 'success' , message : `Saved to ${ targetName } ` } ) ;
756799 } else {
757800 setNoteSaveToast ( { type : 'error' , message : result ?. error || 'Save failed' } ) ;
758801 }
@@ -780,13 +823,16 @@ const App: React.FC = () => {
780823 const defaultApp = getDefaultNotesApp ( ) ;
781824 const obsOk = isObsidianConfigured ( ) ;
782825 const bearOk = getBearSettings ( ) . enabled ;
826+ const octOk = isOctarineConfigured ( ) ;
783827
784828 if ( defaultApp === 'download' ) {
785829 handleDownloadAnnotations ( ) ;
786830 } else if ( defaultApp === 'obsidian' && obsOk ) {
787831 handleQuickSaveToNotes ( 'obsidian' ) ;
788832 } else if ( defaultApp === 'bear' && bearOk ) {
789833 handleQuickSaveToNotes ( 'bear' ) ;
834+ } else if ( defaultApp === 'octarine' && octOk ) {
835+ handleQuickSaveToNotes ( 'octarine' ) ;
790836 } else {
791837 setInitialExportTab ( 'notes' ) ;
792838 setShowExport ( true ) ;
@@ -1025,7 +1071,18 @@ const App: React.FC = () => {
10251071 Save to Bear
10261072 </ button >
10271073 ) }
1028- { isApiMode && ! isObsidianConfigured ( ) && ! getBearSettings ( ) . enabled && (
1074+ { isApiMode && isOctarineConfigured ( ) && (
1075+ < button
1076+ onClick = { ( ) => handleQuickSaveToNotes ( 'octarine' ) }
1077+ className = "w-full text-left px-3 py-1.5 text-xs hover:bg-muted transition-colors flex items-center gap-2"
1078+ >
1079+ < svg className = "w-3.5 h-3.5 text-muted-foreground" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" strokeWidth = { 2 } >
1080+ < path strokeLinecap = "round" strokeLinejoin = "round" d = "M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
1081+ </ svg >
1082+ Save to Octarine
1083+ </ button >
1084+ ) }
1085+ { isApiMode && ! isObsidianConfigured ( ) && ! getBearSettings ( ) . enabled && ! isOctarineConfigured ( ) && (
10291086 < div className = "px-3 py-2 text-[10px] text-muted-foreground" >
10301087 No notes apps configured.
10311088 </ div >
0 commit comments