interbrain-update-modal.ts
1 /** 2 * InterBrain Update Modal 3 * 4 * Simple all-or-nothing update for the InterBrain plugin itself. 5 * Unlike DreamNode cherry-picking, InterBrain updates must be accepted in full 6 * (pull from main, build, reload). Users can request an AI summary before deciding. 7 */ 8 9 import { App, Modal, Setting } from 'obsidian'; 10 import { FetchResult, CommitInfo } from '../../social-resonance-filter/services/git-sync-service'; 11 import { 12 getUpdateSummaryService, 13 initializeUpdateSummaryService, 14 UpdateSummary 15 } from '../services/update-summary-service'; 16 17 export class InterBrainUpdateModal extends Modal { 18 private fetchResult: FetchResult; 19 private onAccept: () => Promise<void>; 20 private onReject: () => void; 21 private summaryEl: HTMLElement | null = null; 22 private isSummaryLoading = false; 23 24 constructor( 25 app: App, 26 fetchResult: FetchResult, 27 onAccept: () => Promise<void>, 28 onReject: () => void 29 ) { 30 super(app); 31 this.fetchResult = fetchResult; 32 this.onAccept = onAccept; 33 this.onReject = onReject; 34 } 35 36 onOpen() { 37 const { contentEl } = this; 38 contentEl.empty(); 39 contentEl.addClass('interbrain-update-modal'); 40 41 // Header 42 contentEl.createEl('h2', { text: '๐ง InterBrain Update' }); 43 44 // Commit count info 45 const infoEl = contentEl.createDiv({ cls: 'interbrain-update-info' }); 46 infoEl.setText( 47 `${this.fetchResult.commits.length} new commit${this.fetchResult.commits.length > 1 ? 's' : ''} available` 48 ); 49 50 // Commit list (expandable) 51 this.renderCommitList(contentEl); 52 53 // Summary container (populated on demand) 54 this.summaryEl = contentEl.createDiv({ cls: 'interbrain-summary-container' }); 55 56 // Action buttons 57 this.renderActions(contentEl); 58 this.addStyles(); 59 } 60 61 private renderCommitList(containerEl: HTMLElement): void { 62 const detailsEl = containerEl.createEl('details', { cls: 'interbrain-commits' }); 63 detailsEl.createEl('summary', { text: 'View commits' }); 64 65 const listEl = detailsEl.createEl('ul'); 66 this.fetchResult.commits.forEach((commit: CommitInfo) => { 67 const li = listEl.createEl('li'); 68 li.createEl('strong', { text: commit.subject }); 69 const date = new Date(commit.timestamp * 1000).toLocaleDateString(); 70 li.createEl('span', { 71 text: ` ยท ${commit.author} ยท ${date}`, 72 cls: 'interbrain-commit-meta' 73 }); 74 }); 75 } 76 77 private renderActions(containerEl: HTMLElement): void { 78 const buttonContainer = containerEl.createDiv({ cls: 'modal-button-container' }); 79 80 // Only show summarize if there are 2+ commits 81 const canSummarize = this.fetchResult.commits.length >= 2; 82 83 new Setting(buttonContainer) 84 .addButton(btn => { 85 btn 86 .setButtonText('๐ Summarize') 87 .onClick(async () => { 88 await this.showSummary(); 89 }); 90 if (!canSummarize) { 91 btn.setDisabled(true); 92 btn.buttonEl.setAttribute('title', 'Need at least 2 commits to summarize'); 93 } 94 }) 95 .addButton(btn => btn 96 .setButtonText('Update & Reload') 97 .setCta() 98 .onClick(async () => { 99 this.close(); 100 await this.onAccept(); 101 })) 102 .addButton(btn => btn 103 .setButtonText('Not Now') 104 .onClick(() => { 105 this.close(); 106 this.onReject(); 107 })); 108 } 109 110 private async showSummary(): Promise<void> { 111 if (!this.summaryEl || this.isSummaryLoading) return; 112 113 this.isSummaryLoading = true; 114 this.summaryEl.empty(); 115 this.summaryEl.createDiv({ 116 cls: 'interbrain-summary-loading', 117 text: 'Generating summary...' 118 }); 119 120 try { 121 initializeUpdateSummaryService(); 122 const summaryService = getUpdateSummaryService(); 123 const summary = await summaryService.generateUpdateSummary(this.fetchResult); 124 125 this.summaryEl.empty(); 126 this.displaySummary(summary); 127 } catch (error) { 128 this.summaryEl.empty(); 129 this.summaryEl.createDiv({ 130 cls: 'interbrain-summary-error', 131 text: 'Failed to generate summary' 132 }); 133 console.error('[InterBrainUpdate] Summary failed:', error); 134 } finally { 135 this.isSummaryLoading = false; 136 } 137 } 138 139 private displaySummary(summary: UpdateSummary): void { 140 if (!this.summaryEl) return; 141 142 // Header 143 this.summaryEl.createEl('strong', { text: "What's been happening:" }); 144 145 // The briefing - just natural prose 146 const contentEl = this.summaryEl.createDiv({ cls: 'interbrain-summary-content' }); 147 const briefingText = summary.briefing || summary.userFacingChanges; 148 contentEl.createEl('p', { text: briefingText }); 149 150 // Stats 151 const statsEl = this.summaryEl.createDiv({ cls: 'interbrain-summary-stats' }); 152 statsEl.setText( 153 `${this.fetchResult.filesChanged} files ยท ` + 154 `+${this.fetchResult.insertions} -${this.fetchResult.deletions}` 155 ); 156 } 157 158 private addStyles(): void { 159 const styleId = 'interbrain-update-modal-styles'; 160 if (document.getElementById(styleId)) return; 161 162 const style = document.createElement('style'); 163 style.id = styleId; 164 style.textContent = ` 165 .interbrain-update-modal { 166 max-width: 500px; 167 } 168 169 .interbrain-update-info { 170 color: var(--text-muted); 171 margin-bottom: 1em; 172 } 173 174 .interbrain-commits { 175 background: var(--background-secondary); 176 padding: 1em; 177 border-radius: 8px; 178 margin: 1em 0; 179 } 180 181 .interbrain-commits summary { 182 cursor: pointer; 183 font-weight: 500; 184 } 185 186 .interbrain-commits ul { 187 margin-top: 0.75em; 188 padding-left: 1.25em; 189 } 190 191 .interbrain-commits li { 192 margin: 0.5em 0; 193 line-height: 1.4; 194 } 195 196 .interbrain-commit-meta { 197 color: var(--text-muted); 198 font-size: 0.85em; 199 } 200 201 .interbrain-summary-container { 202 margin: 1em 0; 203 } 204 205 .interbrain-summary-loading { 206 padding: 1em; 207 text-align: center; 208 color: var(--text-muted); 209 } 210 211 .interbrain-summary-error { 212 padding: 1em; 213 text-align: center; 214 color: var(--text-error); 215 } 216 217 .interbrain-summary-impact { 218 background: var(--background-secondary); 219 padding: 0.75em 1em; 220 border-radius: 6px; 221 margin-bottom: 0.75em; 222 font-weight: 500; 223 } 224 225 .interbrain-summary-content { 226 line-height: 1.6; 227 } 228 229 .interbrain-summary-content p { 230 margin: 0 0 0.5em 0; 231 } 232 233 .interbrain-summary-technical { 234 color: var(--text-muted); 235 font-size: 0.95em; 236 } 237 238 .interbrain-summary-stats { 239 color: var(--text-muted); 240 font-size: 0.85em; 241 margin-top: 0.75em; 242 padding-top: 0.75em; 243 border-top: 1px solid var(--background-modifier-border); 244 } 245 `; 246 document.head.appendChild(style); 247 } 248 249 onClose() { 250 this.contentEl.empty(); 251 } 252 }