feat: ship v0.9.0 with pomodoro, achievements, and injection history

Add pomodoro timer with goal notifications, writing streak milestones, and knowledge injection logging with UI previews across AI writing flows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-07 19:15:52 +08:00
parent 255f1a99a0
commit d4122c8f95
53 changed files with 1679 additions and 80 deletions
@@ -0,0 +1,10 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { AchievementService } from '../../services/achievement.service'
import { wrap } from '../result'
export function registerAchievementHandlers(achievement: AchievementService): void {
ipcMain.handle(IPC.ACHIEVEMENT_LIST, (_e, { bookId }: { bookId: string }) =>
wrap(() => achievement.listAchievements(bookId))
)
}
+7
View File
@@ -9,6 +9,7 @@ import { checkInteractiveGate } from '../../services/interactive-gate.service'
import { AutoWritingService } from '../../services/auto-writing.service'
import { scheduleChapterExtraction } from '../../services/knowledge-extraction-runner'
import { trackerFor } from '../../services/chapter-writing-tracker.service'
import { recordKnowledgeInjection } from '../../services/knowledge-injection-runner'
import type { WritingLogService } from '../../services/writing-log.service'
import { InteractiveRepository } from '../../db/repositories/interactive.repo'
import { wrap } from '../result'
@@ -89,6 +90,12 @@ export function registerAutoHandlers(
bookId,
settings.get().penName
)
recordKnowledgeInjection(registry, bookId, {
knowledgeIds: binding.knowledgeIds,
flowMode: 'auto',
flowId,
chapterId: registry.getMeta(bookId)?.lastChapterId ?? undefined
})
return svc.enrichFlow(svc.confirmContext(flowId, payload))
})
)
+12 -2
View File
@@ -3,19 +3,29 @@ import { IPC } from '../../../shared/ipc-channels'
import { BookRegistryService } from '../../services/book-registry'
import { CockpitService } from '../../services/cockpit.service'
import type { GlobalSettingsService } from '../../services/global-settings'
import type { AchievementService } from '../../services/achievement.service'
import type { PomodoroDailyRepository } from '../../db/repositories/pomodoro-daily.repo'
import type { WritingLogService } from '../../services/writing-log.service'
import { wrap } from '../result'
export function registerCockpitHandlers(
registry: BookRegistryService,
settings: GlobalSettingsService,
writingLogs: WritingLogService
writingLogs: WritingLogService,
achievementService: AchievementService,
pomodoroDaily: PomodoroDailyRepository
): void {
ipcMain.handle(
IPC.COCKPIT_SUMMARY,
(_e, { bookId, volumeId }: { bookId: string; volumeId?: string }) =>
wrap(() =>
new CockpitService(registry.getDb(bookId), settings, writingLogs).getSummary(volumeId, bookId)
new CockpitService(
registry.getDb(bookId),
settings,
writingLogs,
achievementService,
pomodoroDaily
).getSummary(volumeId, bookId)
)
)
@@ -9,6 +9,7 @@ import { checkInteractiveGate } from '../../services/interactive-gate.service'
import { InteractiveWritingService } from '../../services/interactive-writing.service'
import { scheduleChapterExtraction } from '../../services/knowledge-extraction-runner'
import { trackerFor } from '../../services/chapter-writing-tracker.service'
import { recordKnowledgeInjection } from '../../services/knowledge-injection-runner'
import type { WritingLogService } from '../../services/writing-log.service'
import { wrap } from '../result'
@@ -66,6 +67,12 @@ export function registerInteractiveHandlers(
bookId,
settings.get().penName
)
recordKnowledgeInjection(registry, bookId, {
knowledgeIds: binding.knowledgeIds,
flowMode: 'interactive',
flowId,
chapterId: registry.getMeta(bookId)?.lastChapterId ?? undefined
})
const flow = serviceFor(registry, bookId, aiClient).confirmContext(flowId, payload)
return serviceFor(registry, bookId, aiClient).enrichFlow(flow)
})
@@ -11,6 +11,7 @@ import type { AiClientService } from '../../services/ai-client.service'
import type { GlobalSettingsService } from '../../services/global-settings'
import { KnowledgeRepository } from '../../db/repositories/knowledge.repo'
import { KnowledgeExtractionService } from '../../services/knowledge-extraction.service'
import { KnowledgeInjectionService } from '../../services/knowledge-injection.service'
import { KnowledgeRetrievalService } from '../../services/knowledge-retrieval.service'
import { KnowledgeService } from '../../services/knowledge.service'
import { wrap } from '../result'
@@ -120,4 +121,15 @@ export function registerKnowledgeHandlers(
return new KnowledgeService(registry.getDb(bookId)).batchApproveHighConfidence(t)
})
)
ipcMain.handle(
IPC.KNOWLEDGE_LIST_INJECTIONS,
(
_e,
{ bookId, entryId, limit }: { bookId: string; entryId: string; limit?: number }
) =>
wrap(() =>
new KnowledgeInjectionService(registry.getDb(bookId)).listForEntry(entryId, limit ?? 50)
)
)
}
+14
View File
@@ -0,0 +1,14 @@
import { ipcMain } from 'electron'
import { IPC } from '../../../shared/ipc-channels'
import type { PomodoroService } from '../../services/pomodoro.service'
import { wrap } from '../result'
export function registerPomodoroHandlers(pomodoro: PomodoroService): void {
ipcMain.handle(IPC.POMODORO_START, (_e, { bookId }: { bookId: string }) =>
wrap(() => pomodoro.start(bookId))
)
ipcMain.handle(IPC.POMODORO_PAUSE, () => wrap(() => pomodoro.pause()))
ipcMain.handle(IPC.POMODORO_RESUME, () => wrap(() => pomodoro.resume()))
ipcMain.handle(IPC.POMODORO_CANCEL, () => wrap(() => pomodoro.cancel()))
ipcMain.handle(IPC.POMODORO_GET_STATE, () => wrap(() => pomodoro.getState()))
}
+7
View File
@@ -6,6 +6,7 @@ import { GlobalSettingsService } from '../../services/global-settings'
import { AiClientService } from '../../services/ai-client.service'
import { buildFlowContextPayload, readFlowSystemPrompt } from '../../services/flow-context.util'
import { WizardWritingService } from '../../services/wizard-writing.service'
import { recordKnowledgeInjection } from '../../services/knowledge-injection-runner'
import { InteractiveRepository } from '../../db/repositories/interactive.repo'
import { wrap } from '../result'
@@ -99,6 +100,12 @@ export function registerWizardHandlers(
bookId,
settings.get().penName
)
recordKnowledgeInjection(registry, bookId, {
knowledgeIds: binding.knowledgeIds,
flowMode: 'wizard',
flowId,
chapterId: registry.getMeta(bookId)?.lastChapterId ?? undefined
})
const svc = serviceFor(registry, bookId, aiClient)
return svc.enrichFlow(svc.confirmContext(flowId, payload))
})
+24 -3
View File
@@ -1,11 +1,15 @@
import { join } from 'path'
import { app } from 'electron'
import { GlobalSettingsService } from '../services/global-settings'
import { BookRegistryService } from '../services/book-registry'
import { SnapshotService } from '../services/snapshot.service'
import { ShortcutManager } from '../shortcuts/manager'
import { WritingLogRepository } from '../db/repositories/writing-log.repo'
import { WritingMilestoneRepository } from '../db/repositories/writing-milestone.repo'
import { PomodoroDailyRepository } from '../db/repositories/pomodoro-daily.repo'
import { WritingLogService } from '../services/writing-log.service'
import { GoalNotificationService } from '../services/goal-notification.service'
import { AchievementService } from '../services/achievement.service'
import { PomodoroService } from '../services/pomodoro.service'
import { registerSettingsHandlers } from './handlers/settings.handler'
import { registerBookHandlers } from './handlers/book.handler'
import { registerChapterHandlers } from './handlers/chapter.handler'
@@ -23,6 +27,8 @@ import { registerWizardHandlers } from './handlers/wizard.handler'
import { registerKnowledgeHandlers } from './handlers/knowledge.handler'
import { registerCockpitHandlers } from './handlers/cockpit.handler'
import { registerWritingHandlers } from './handlers/writing.handler'
import { registerPomodoroHandlers } from './handlers/pomodoro.handler'
import { registerAchievementHandlers } from './handlers/achievement.handler'
import { registerBridgeHandlers } from './handlers/bridge.handler'
import { AiClientService } from '../services/ai-client.service'
import { NetworkMonitorService } from '../services/network-monitor.service'
@@ -36,7 +42,20 @@ export function registerIpc(): { settings: GlobalSettingsService; books: BookReg
const settings = new GlobalSettingsService(userData)
const books = new BookRegistryService(userData)
const writingLogs = new WritingLogService(new WritingLogRepository(userData), settings)
const writingLogRepo = new WritingLogRepository(userData)
const writingLogs = new WritingLogService(writingLogRepo, settings)
const milestoneRepo = new WritingMilestoneRepository(writingLogRepo.getDb())
const pomodoroDailyRepo = new PomodoroDailyRepository(writingLogRepo.getDb())
const goalNotify = new GoalNotificationService(settings)
const achievementService = new AchievementService(
milestoneRepo,
writingLogs,
settings,
goalNotify
)
writingLogs.setGoalHooks(achievementService, goalNotify)
const pomodoro = new PomodoroService(writingLogs, pomodoroDailyRepo, settings, goalNotify)
shortcutManager = new ShortcutManager(settings)
snapshotService = new SnapshotService(() => settings.get())
networkMonitor = new NetworkMonitorService()
@@ -57,8 +76,10 @@ export function registerIpc(): { settings: GlobalSettingsService; books: BookReg
registerAutoHandlers(books, settings, aiClient, writingLogs)
registerWizardHandlers(books, settings, aiClient)
registerKnowledgeHandlers(books, settings, aiClient)
registerCockpitHandlers(books, settings, writingLogs)
registerCockpitHandlers(books, settings, writingLogs, achievementService, pomodoroDailyRepo)
registerWritingHandlers(writingLogs)
registerPomodoroHandlers(pomodoro)
registerAchievementHandlers(achievementService)
registerBridgeHandlers(books, aiClient)
return { settings, books, shortcuts: shortcutManager }