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
@@ -4,9 +4,11 @@ import type {
CreateKnowledgeInput,
ForeshadowStatus,
KnowledgeEntry,
KnowledgeInjectionLog,
KnowledgeType
} from '@shared/types'
import { useBookStore } from '@renderer/stores/useBookStore'
import { useEditStore } from '@renderer/stores/useEditStore'
import { useKnowledgeStore } from '@renderer/stores/useKnowledgeStore'
import { ipcCall } from '@renderer/lib/ipc-client'
@@ -26,6 +28,7 @@ export function KnowledgeEditorDialog({
const { t } = useTranslation()
const bookId = useBookStore((s) => s.currentBookId)
const chapters = useBookStore((s) => s.chapters)
const switchTarget = useEditStore((s) => s.switchTarget)
const entries = useKnowledgeStore((s) => s.entries)
const create = useKnowledgeStore((s) => s.create)
const update = useKnowledgeStore((s) => s.update)
@@ -45,6 +48,17 @@ export function KnowledgeEditorDialog({
const [foreshadowStatus, setForeshadowStatus] = useState<ForeshadowStatus>('buried')
const [sourceChapterId, setSourceChapterId] = useState('')
const [approveNow, setApproveNow] = useState(false)
const [injections, setInjections] = useState<KnowledgeInjectionLog[]>([])
useEffect(() => {
if (!open || !bookId || !entryId) {
setInjections([])
return
}
void ipcCall(() => window.electronAPI.knowledge.listInjections(bookId, entryId)).then(
setInjections
)
}, [open, bookId, entryId])
useEffect(() => {
if (!open) return
@@ -183,6 +197,39 @@ export function KnowledgeEditorDialog({
{t('knowledge.approveNow')}
</label>
)}
{existing && injections.length > 0 && (
<div className="knowledge-injection-history" data-testid="knowledge-injection-history">
<div className="kb-injection-label">{t('knowledge.injectionHistory')}</div>
{injections.map((log) => {
const chapterTitle =
chapters.find((c) => c.id === log.chapterId)?.title ?? t('knowledge.noChapter')
return (
<div key={log.id} className="kb-injection-row">
<span className="kb-injection-date">
{new Date(log.injectedAt).toLocaleString()}
</span>
<span className="kb-injection-mode">
{t(`knowledge.injectionMode.${log.flowMode}`)}
</span>
{log.chapterId ? (
<button
type="button"
className="kb-injection-chapter-link"
onClick={() => {
void switchTarget({ kind: 'chapter', id: log.chapterId! })
onClose()
}}
>
{chapterTitle}
</button>
) : (
<span>{chapterTitle}</span>
)}
</div>
)
})}
</div>
)}
</div>
<div className="modal-footer">
{isMergeMode && bookId && existing && (
@@ -13,6 +13,7 @@ import {
export function KnowledgePanel(): React.JSX.Element {
const { t } = useTranslation()
const bookId = useBookStore((s) => s.currentBookId)
const chapters = useBookStore((s) => s.chapters)
const selectedChapterId = useBookStore((s) => s.selectedChapterId)
const extractThreshold = useSettingsStore((s) => s.knowledgeExtractConfidenceThreshold)
const {
@@ -32,6 +33,8 @@ export function KnowledgePanel(): React.JSX.Element {
extractChapter,
batchApproveHighConfidence
} = useKnowledgeStore()
const injectionPreviews = useKnowledgeStore((s) => s.injectionPreviews)
const loadInjectionPreviews = useKnowledgeStore((s) => s.loadInjectionPreviews)
const [extracting, setExtracting] = useState(false)
const [namingOpen, setNamingOpen] = useState(false)
const [selected, setSelected] = useState<Set<string>>(new Set())
@@ -51,6 +54,14 @@ export function KnowledgePanel(): React.JSX.Element {
)
}, [bookId, entries])
useEffect(() => {
if (!bookId || entries.length === 0) return
void loadInjectionPreviews(
bookId,
entries.map((e) => e.id)
)
}, [bookId, entries, loadInjectionPreviews])
const handleBatchApprove = async (): Promise<void> => {
if (!bookId || selected.size === 0) return
await batchApprove(bookId, [...selected])
@@ -188,6 +199,25 @@ export function KnowledgePanel(): React.JSX.Element {
</div>
)}
{entry.content && <div className="kb-item-content">{entry.content}</div>}
{(injectionPreviews[entry.id]?.length ?? 0) > 0 && (
<div
className="kb-injection-preview"
data-testid={`knowledge-injection-preview-${entry.id}`}
>
<div className="kb-injection-label">{t('knowledge.injectionPreview')}</div>
{injectionPreviews[entry.id]!.map((log) => (
<div key={log.id} className="kb-injection-preview-row">
{t('knowledge.injectionEntry', {
date: new Date(log.injectedAt).toLocaleDateString(),
mode: t(`knowledge.injectionMode.${log.flowMode}`),
chapter:
chapters.find((c) => c.id === log.chapterId)?.title ??
t('knowledge.noChapter')
})}
</div>
))}
</div>
)}
</div>
<div className="kb-item-actions">
{entry.status === 'pending' && bookId && (