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
@@ -10,6 +10,7 @@ import { useReferenceStore } from '@renderer/stores/useReferenceStore'
import { useSettingsStore } from '@renderer/stores/useSettingsStore'
import { useCockpitStore } from '@renderer/stores/useCockpitStore'
import { useWritingStatsStore } from '@renderer/stores/useWritingStatsStore'
import { usePomodoroStore, formatPomodoroTime } from '@renderer/stores/usePomodoroStore'
import { ipcCall } from '@renderer/lib/ipc-client'
import { TipTapEditor, flushEditorSave } from '@renderer/components/editor/TipTapEditor'
import { insertLandmarkInEditor } from '@renderer/lib/editor-commands'
@@ -94,6 +95,15 @@ export function EditorLayout(): React.JSX.Element {
const dailyWordGoal = useSettingsStore((s) => s.dailyWordGoal)
const writingStats = useWritingStatsStore((s) => s.stats)
const refreshWritingStats = useWritingStatsStore((s) => s.refresh)
const pomodoroRunning = usePomodoroStore((s) => s.running)
const pomodoroPaused = usePomodoroStore((s) => s.paused)
const pomodoroRemaining = usePomodoroStore((s) => s.remainingSec)
const pomodoroBookId = usePomodoroStore((s) => s.bookId)
const pomodoroInit = usePomodoroStore((s) => s.init)
const pomodoroStart = usePomodoroStore((s) => s.start)
const pomodoroPause = usePomodoroStore((s) => s.pause)
const pomodoroResume = usePomodoroStore((s) => s.resume)
const pomodoroCancel = usePomodoroStore((s) => s.cancel)
const bridgeChapterId = useCockpitStore((s) => s.bridgeChapterId)
const clearBridgeChapter = useCockpitStore((s) => s.clearBridgeChapter)
const updateChapterLocal = useBookStore((s) => s.updateChapterLocal)
@@ -111,11 +121,21 @@ export function EditorLayout(): React.JSX.Element {
const isChapterTarget = target?.kind === 'chapter'
const chapterId = target?.kind === 'chapter' ? target.id : selectedChapterId
useEffect(() => {
void pomodoroInit()
}, [pomodoroInit])
useEffect(() => {
if (!pomodoroRunning || !pomodoroBookId || !currentBookId) return
if (pomodoroBookId === currentBookId) return
void pomodoroCancel()
}, [currentBookId, pomodoroRunning, pomodoroBookId, pomodoroCancel])
useEffect(() => {
if (selectedChapterId && (!target || target.kind === 'chapter')) {
setTarget({ kind: 'chapter', id: selectedChapterId })
}
}, [selectedChapterId, setTarget])
}, [selectedChapterId, setTarget, target])
useEffect(() => {
if (!currentBookId || target?.kind !== 'chapter') return
@@ -426,6 +446,39 @@ export function EditorLayout(): React.JSX.Element {
bookId={currentBookId}
/>
<div id="editor-statusbar">
<div className="pomodoro-control" data-testid="pomodoro-control">
{!pomodoroRunning ? (
<button
type="button"
className="pomodoro-btn"
title={t('pomodoro.start')}
disabled={!currentBookId}
onClick={() => currentBookId && void pomodoroStart(currentBookId)}
>
</button>
) : (
<>
<button
type="button"
className="pomodoro-btn"
title={pomodoroPaused ? t('pomodoro.resume') : t('pomodoro.pause')}
onClick={() => void (pomodoroPaused ? pomodoroResume() : pomodoroPause())}
>
{pomodoroPaused ? '▶' : '⏸'}
</button>
<span className="pomodoro-time">{formatPomodoroTime(pomodoroRemaining)}</span>
<button
type="button"
className="pomodoro-btn"
title={t('pomodoro.cancel')}
onClick={() => void pomodoroCancel()}
>
</button>
</>
)}
</div>
<span>{saveStatus}{lastSaved ? ` · ${lastSaved.toLocaleTimeString()}` : ''}</span>
{dailyWordGoal > 0 && writingStats && (
<span