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))
})