@@ -13,6 +13,7 @@ const { fileURLToPath, pathToFileURL } = require("node:url");
1313const { updateElectronApp, UpdateSourceType } = require ( "update-electron-app" ) ;
1414const {
1515 bundlePaths,
16+ contextPacketPaths,
1617 readJSONIfExists,
1718 readTextIfExists,
1819 sourceTrackPaths,
@@ -1429,6 +1430,111 @@ function normalizeStoredAppSessionStatus(value) {
14291430 } ;
14301431}
14311432
1433+ function normalizeSourceContextStatus ( value ) {
1434+ const raw = value && typeof value === "object" ? value : { } ;
1435+ const state = optionalNonEmptyString ( raw . state ) ;
1436+ if ( ! [ "idle" , "generating" , "ready" , "failed" ] . includes ( state ) ) {
1437+ return null ;
1438+ }
1439+ return {
1440+ state,
1441+ detail : optionalNonEmptyString ( raw . detail ) ,
1442+ retryable : Boolean ( raw . retryable ) ,
1443+ updatedAt : optionalNonEmptyString ( raw . updatedAt ) ,
1444+ packetId : optionalNonEmptyString ( raw . packetId ) ,
1445+ objectCount : Number . isFinite ( Number ( raw . objectCount ) ) ? Number ( raw . objectCount ) : 0 ,
1446+ relationCount : Number . isFinite ( Number ( raw . relationCount ) ) ? Number ( raw . relationCount ) : 0 ,
1447+ warningCount : Number . isFinite ( Number ( raw . warningCount ) ) ? Number ( raw . warningCount ) : 0 ,
1448+ agentBriefPath : null ,
1449+ projectContextPath : null ,
1450+ contextDirectoryPath : null ,
1451+ } ;
1452+ }
1453+
1454+ function withDerivedSourceContextPaths ( status , directoryPath , availablePaths = { } ) {
1455+ const paths = contextPacketPaths ( directoryPath ) ;
1456+ return {
1457+ ...status ,
1458+ agentBriefPath : availablePaths . agentBrief === false ? null : paths . agentBriefPath ,
1459+ projectContextPath : availablePaths . projectContext === false ? null : paths . projectContextPath ,
1460+ contextDirectoryPath : paths . contextDirectoryPath ,
1461+ } ;
1462+ }
1463+
1464+ function sourceContextStatusFromMetadata ( metadata , directoryPath , options = { } ) {
1465+ const verifyFiles = options . verifyFiles !== false ;
1466+ const paths = contextPacketPaths ( directoryPath ) ;
1467+ const stored = normalizeSourceContextStatus ( metadata ?. sourceContext ) ;
1468+ if ( ! verifyFiles ) {
1469+ if ( stored ) {
1470+ return stored . state === "ready" ? withDerivedSourceContextPaths ( stored , directoryPath ) : stored ;
1471+ }
1472+ return {
1473+ state : "idle" ,
1474+ detail : null ,
1475+ retryable : false ,
1476+ updatedAt : null ,
1477+ packetId : null ,
1478+ objectCount : 0 ,
1479+ relationCount : 0 ,
1480+ warningCount : 0 ,
1481+ agentBriefPath : null ,
1482+ projectContextPath : null ,
1483+ contextDirectoryPath : paths . contextDirectoryPath ,
1484+ } ;
1485+ }
1486+
1487+ const hasAgentBrief = fsSync . existsSync ( paths . agentBriefPath ) ;
1488+ const hasProjectContext = fsSync . existsSync ( paths . projectContextPath ) ;
1489+ if ( hasAgentBrief || hasProjectContext ) {
1490+ return withDerivedSourceContextPaths ( {
1491+ state : "ready" ,
1492+ detail : null ,
1493+ retryable : false ,
1494+ updatedAt : optionalNonEmptyString ( metadata ?. sourceContext ?. updatedAt ) ,
1495+ packetId : optionalNonEmptyString ( metadata ?. sourceContext ?. packetId ) ,
1496+ objectCount : Number . isFinite ( Number ( metadata ?. sourceContext ?. objectCount ) ) ? Number ( metadata . sourceContext . objectCount ) : 0 ,
1497+ relationCount : Number . isFinite ( Number ( metadata ?. sourceContext ?. relationCount ) ) ? Number ( metadata . sourceContext . relationCount ) : 0 ,
1498+ warningCount : Number . isFinite ( Number ( metadata ?. sourceContext ?. warningCount ) ) ? Number ( metadata . sourceContext . warningCount ) : 0 ,
1499+ agentBriefPath : null ,
1500+ projectContextPath : null ,
1501+ contextDirectoryPath : null ,
1502+ } , directoryPath , { agentBrief : hasAgentBrief , projectContext : hasProjectContext } ) ;
1503+ }
1504+
1505+ if ( stored && stored . state !== "ready" ) {
1506+ return stored ;
1507+ }
1508+
1509+ return {
1510+ state : "idle" ,
1511+ detail : null ,
1512+ retryable : false ,
1513+ updatedAt : null ,
1514+ packetId : null ,
1515+ objectCount : 0 ,
1516+ relationCount : 0 ,
1517+ warningCount : 0 ,
1518+ agentBriefPath : null ,
1519+ projectContextPath : null ,
1520+ contextDirectoryPath : paths . contextDirectoryPath ,
1521+ } ;
1522+ }
1523+
1524+ function noteHasCompletedTranscriptForSourceContext ( note ) {
1525+ const sessionState = optionalNonEmptyString ( note ?. sessionStatus ?. state ) ;
1526+ if ( [ "recording" , "degraded" , "finalizing" , "transcribing" ] . includes ( sessionState ) ) {
1527+ return false ;
1528+ }
1529+ return sessionState === "completed"
1530+ && Array . isArray ( note ?. transcriptSegments )
1531+ && note . transcriptSegments . some ( ( segment ) => ! segment . isPreview && typeof segment . text === "string" && segment . text . trim ( ) ) ;
1532+ }
1533+
1534+ function errorMessage ( error , fallback = "Source context generation failed." ) {
1535+ return error instanceof Error && error . message ? error . message : fallback ;
1536+ }
1537+
14321538async function writeListenerSessionState ( noteId , patch ) {
14331539 if ( ! noteId ) {
14341540 return ;
@@ -1898,6 +2004,7 @@ async function loadBundleSnapshot(directoryName) {
18982004 recordingPath : paths . recordingPath ,
18992005 listenerSession,
19002006 sessionStatus : normalizeAppSessionStatus ( { listenerSession, sttChunks } ) ,
2007+ sourceContextStatus : sourceContextStatusFromMetadata ( metadata , directoryPath ) ,
19012008 } ;
19022009
19032010 return {
@@ -1930,6 +2037,7 @@ function noteSummaryFromRow(row) {
19302037 recordingPath : row . recording_path ,
19312038 listenerSession,
19322039 sessionStatus : normalizeStoredAppSessionStatus ( row . session_status_json ) ,
2040+ sourceContextStatus : sourceContextStatusFromMetadata ( metadata , row . directory_path , { verifyFiles : false } ) ,
19332041 } ;
19342042}
19352043
@@ -2540,6 +2648,7 @@ async function readNote(id) {
25402648 return {
25412649 ...summary ,
25422650 sessionStatus : normalizeAppSessionStatus ( { listenerSession : summary . listenerSession , sttChunks } ) ,
2651+ sourceContextStatus : sourceContextStatusFromMetadata ( parseMetadataJSON ( row . metadata_json ) , directoryPath ) ,
25432652 directoryPath,
25442653 markdown : row . markdown ,
25452654 metadata : parseMetadataJSON ( row . metadata_json ) ,
@@ -3057,17 +3166,96 @@ async function generateNoteSummary(noteID, templateID) {
30573166
30583167async function generateSourceContext ( noteID ) {
30593168 const note = await readNote ( noteID ) ;
3169+ const directoryPath = resolveBundleDirectory ( noteID ) ;
3170+ const paths = bundlePaths ( directoryPath ) ;
3171+ const currentMetadata = await readJSONIfExists ( paths . metadataPath ) || { } ;
3172+ const currentStatus = sourceContextStatusFromMetadata ( currentMetadata , directoryPath ) ;
3173+ if ( currentStatus . state === "ready" ) {
3174+ return {
3175+ noteId : note . id ,
3176+ sourceContextStatus : currentStatus ,
3177+ } ;
3178+ }
3179+ if ( ! noteHasCompletedTranscriptForSourceContext ( note ) ) {
3180+ const blockedStatus = {
3181+ state : "failed" ,
3182+ detail : "No completed transcript is available for source context generation." ,
3183+ retryable : true ,
3184+ updatedAt : new Date ( ) . toISOString ( ) ,
3185+ } ;
3186+ currentMetadata . sourceContext = blockedStatus ;
3187+ await writeJSONFileAtomic ( paths . metadataPath , currentMetadata ) ;
3188+ await syncNotesDatabaseFromFiles ( [ noteID ] ) ;
3189+ throw new Error ( blockedStatus . detail ) ;
3190+ }
3191+
3192+ currentMetadata . sourceContext = {
3193+ state : "generating" ,
3194+ detail : null ,
3195+ retryable : false ,
3196+ updatedAt : new Date ( ) . toISOString ( ) ,
3197+ } ;
3198+ await writeJSONFileAtomic ( paths . metadataPath , currentMetadata ) ;
3199+ await syncNotesDatabaseFromFiles ( [ noteID ] ) ;
3200+
30603201 const settings = await readElectronSettings ( ) ;
30613202 const summarySettings = normalizeSummarySettings ( settings . summary ) ;
3062- const payload = await nativeCaptureRequest ( "generateSourceContext" , {
3063- targetDirectory : note . directoryPath ,
3064- summarySettings,
3065- } ) ;
3203+ try {
3204+ const payload = await nativeCaptureRequest ( "generateSourceContext" , {
3205+ targetDirectory : note . directoryPath ,
3206+ summarySettings,
3207+ } ) ;
3208+ const sourceContext = payload . sourceContext || payload ;
3209+ const nextMetadata = await readJSONIfExists ( paths . metadataPath ) || { } ;
3210+ nextMetadata . sourceContext = {
3211+ state : "ready" ,
3212+ detail : null ,
3213+ retryable : false ,
3214+ updatedAt : new Date ( ) . toISOString ( ) ,
3215+ packetId : optionalNonEmptyString ( sourceContext . packetId ) ,
3216+ objectCount : Number . isFinite ( Number ( sourceContext . objectCount ) ) ? Number ( sourceContext . objectCount ) : 0 ,
3217+ relationCount : Number . isFinite ( Number ( sourceContext . relationCount ) ) ? Number ( sourceContext . relationCount ) : 0 ,
3218+ warningCount : Number . isFinite ( Number ( sourceContext . warningCount ) ) ? Number ( sourceContext . warningCount ) : 0 ,
3219+ } ;
3220+ await writeJSONFileAtomic ( paths . metadataPath , nextMetadata ) ;
3221+ await syncNotesDatabaseFromFiles ( [ noteID ] ) ;
30663222
3067- return {
3068- noteId : note . id ,
3069- ...( payload . sourceContext || payload ) ,
3070- } ;
3223+ return {
3224+ noteId : note . id ,
3225+ ...sourceContext ,
3226+ sourceContextStatus : sourceContextStatusFromMetadata ( nextMetadata , directoryPath ) ,
3227+ } ;
3228+ } catch ( error ) {
3229+ const message = errorMessage ( error ) ;
3230+ const nextMetadata = await readJSONIfExists ( paths . metadataPath ) || { } ;
3231+ nextMetadata . sourceContext = {
3232+ state : "failed" ,
3233+ detail : message ,
3234+ retryable : true ,
3235+ updatedAt : new Date ( ) . toISOString ( ) ,
3236+ } ;
3237+ await writeJSONFileAtomic ( paths . metadataPath , nextMetadata ) ;
3238+ await syncNotesDatabaseFromFiles ( [ noteID ] ) ;
3239+ throw error ;
3240+ }
3241+ }
3242+
3243+ async function openAgentBrief ( noteID ) {
3244+ const directoryPath = resolveBundleDirectory ( noteID ) ;
3245+ const paths = contextPacketPaths ( directoryPath ) ;
3246+ const targetPath = fsSync . existsSync ( paths . agentBriefPath )
3247+ ? paths . agentBriefPath
3248+ : fsSync . existsSync ( paths . projectContextPath )
3249+ ? paths . projectContextPath
3250+ : null ;
3251+ if ( ! targetPath ) {
3252+ throw new Error ( "No agent context file is available for this note." ) ;
3253+ }
3254+ const result = await shell . openPath ( targetPath ) ;
3255+ if ( result ) {
3256+ throw new Error ( result ) ;
3257+ }
3258+ return true ;
30713259}
30723260
30733261async function testSummaryProvider ( ) {
@@ -4522,6 +4710,8 @@ function registerIPCHandlers() {
45224710 ipcMain . handle ( "notes:read" , ( _event , id ) => readNote ( id ) ) ;
45234711 ipcMain . handle ( "notes:save" , ( _event , id , markdown ) => saveNote ( id , markdown ) ) ;
45244712 ipcMain . handle ( "summary:generate" , ( _event , id , templateId ) => generateNoteSummary ( id , templateId ) ) ;
4713+ ipcMain . handle ( "source-context:generate" , ( _event , id ) => generateSourceContext ( id ) ) ;
4714+ ipcMain . handle ( "source-context:open-agent-brief" , ( _event , id ) => openAgentBrief ( id ) ) ;
45254715 ipcMain . handle ( "summary:test-provider" , ( ) => testSummaryProvider ( ) ) ;
45264716 ipcMain . handle ( "summary:list-models" , ( _event , provider ) => listSummaryModels ( provider ) ) ;
45274717 ipcMain . handle ( "transcript:save" , ( _event , id , segments ) => saveTranscript ( id , segments ) ) ;
@@ -4630,7 +4820,9 @@ if (process.env.MIRROR_NOTE_TEST_EXPORTS === "1") {
46304820 normalizeCustomSummaryTemplates,
46314821 normalizeAppSessionStatus,
46324822 normalizeListenerSessionState,
4823+ normalizeSourceContextStatus,
46334824 normalizeSummarySettings,
4825+ noteHasCompletedTranscriptForSourceContext,
46344826 nativeHelperWorkingDirectory,
46354827 normalizeTranscriptSegments,
46364828 parseSTTChunkLedgerJSONL,
@@ -4642,6 +4834,7 @@ if (process.env.MIRROR_NOTE_TEST_EXPORTS === "1") {
46424834 renderSummaryResultMarkdown,
46434835 requestChatCompletion,
46444836 retryTranscript,
4837+ sourceContextStatusFromMetadata,
46454838 uploadRecordingAsset,
46464839 uploadTranscriptAsset,
46474840 uploadMarkdownNote,
0 commit comments