feat: ship v1.2.0 Wave 1 foundation with book settings, chapter management, and focus mode

Deliver schema v9, book/chapter IPC extensions, BookSettingsTab, chapter meta/tags,
AI session menu, focus mode and TTS, template context enrich, and Wave 1 E2E coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-08 15:22:58 +08:00
parent 64da95f7e1
commit fe127ec3ed
57 changed files with 1963 additions and 139 deletions
+6 -4
View File
@@ -19,7 +19,7 @@ interface AiStore {
listenersAttached: boolean
mount: () => void
unmount: () => void
loadSessions: (bookId: string) => Promise<void>
loadSessions: (bookId: string, includeArchived?: boolean) => Promise<void>
selectSession: (sessionId: string) => Promise<void>
createSession: (bookId: string) => Promise<void>
sendMessage: (content: string, prompt?: string) => Promise<void>
@@ -63,8 +63,10 @@ export const useAiStore = create<AiStore>((set, get) => ({
set({ listenersAttached: false })
},
loadSessions: async (bookId) => {
const sessions = await ipcCall(() => window.electronAPI.ai.sessionList(bookId))
loadSessions: async (bookId, includeArchived = true) => {
const sessions = await ipcCall(() =>
window.electronAPI.ai.sessionList(bookId, includeArchived)
)
const activeSessionId = get().activeSessionId
const stillActive = activeSessionId && sessions.some((s) => s.id === activeSessionId)
set({ sessions })
@@ -157,7 +159,7 @@ export const useAiStore = create<AiStore>((set, get) => ({
try {
await ipcCall(() => window.electronAPI.ai.chat(bookId, sessionId, trimmed, prompt))
const messages = await ipcCall(() => window.electronAPI.ai.messageList(bookId, sessionId))
const sessions = await ipcCall(() => window.electronAPI.ai.sessionList(bookId))
const sessions = await ipcCall(() => window.electronAPI.ai.sessionList(bookId, true))
set({ messages, sessions, streamingMessageId: null, streamingBuffer: '' })
} finally {
set({ sending: false })
+8
View File
@@ -10,6 +10,8 @@ interface AppState {
landmarksModalOpen: boolean
inspirationModalOpen: boolean
importBookModalOpen: boolean
focusMode: boolean
namingCheckOpen: boolean
toast: string | null
setView: (view: AppView) => void
setSidebarPanel: (panel: AppState['sidebarPanel']) => void
@@ -18,6 +20,8 @@ interface AppState {
setLandmarksModalOpen: (open: boolean) => void
setInspirationModalOpen: (open: boolean) => void
setImportBookModalOpen: (open: boolean) => void
setFocusMode: (on: boolean) => void
setNamingCheckOpen: (open: boolean) => void
showToast: (message: string) => void
clearToast: () => void
}
@@ -30,6 +34,8 @@ export const useAppStore = create<AppState>((set) => ({
landmarksModalOpen: false,
inspirationModalOpen: false,
importBookModalOpen: false,
focusMode: false,
namingCheckOpen: false,
toast: null,
setView: (view) => set({ view }),
setSidebarPanel: (sidebarPanel) => set({ sidebarPanel }),
@@ -38,6 +44,8 @@ export const useAppStore = create<AppState>((set) => ({
setLandmarksModalOpen: (landmarksModalOpen) => set({ landmarksModalOpen }),
setInspirationModalOpen: (inspirationModalOpen) => set({ inspirationModalOpen }),
setImportBookModalOpen: (importBookModalOpen) => set({ importBookModalOpen }),
setFocusMode: (focusMode) => set({ focusMode }),
setNamingCheckOpen: (namingCheckOpen) => set({ namingCheckOpen }),
showToast: (toast) => {
set({ toast })
setTimeout(() => set({ toast: null }), 2500)
+28
View File
@@ -0,0 +1,28 @@
import { create } from 'zustand'
import { ipcCall } from '@renderer/lib/ipc-client'
interface BookPrefsState {
alwaysShowCockpit: boolean
loadedBookId: string | null
load: (bookId: string) => Promise<void>
setAlwaysShowCockpit: (bookId: string, value: boolean) => Promise<void>
}
export const useBookPrefsStore = create<BookPrefsState>((set, get) => ({
alwaysShowCockpit: false,
loadedBookId: null,
load: async (bookId) => {
const value = await ipcCall(() =>
window.electronAPI.bookPrefs.get(bookId, 'alwaysShowCockpit')
)
set({ alwaysShowCockpit: value === 'true', loadedBookId: bookId })
},
setAlwaysShowCockpit: async (bookId, value) => {
await ipcCall(() =>
window.electronAPI.bookPrefs.set(bookId, 'alwaysShowCockpit', value ? 'true' : 'false')
)
if (get().loadedBookId === bookId) {
set({ alwaysShowCockpit: value })
}
}
}))