diff --git a/src/renderer/components/AIChat/Chat.css b/src/renderer/components/AIChat/Chat.css
index fbbccc7..9784d5e 100644
--- a/src/renderer/components/AIChat/Chat.css
+++ b/src/renderer/components/AIChat/Chat.css
@@ -475,3 +475,69 @@
.chat-send-btn:not(:disabled):hover {
opacity: 0.85;
}
+
+/* ── Suggestion summary block inside assistant messages ─── */
+.chat-message-suggestions {
+ margin-top: 6px;
+ border-top: 1px solid var(--border);
+ padding-top: 7px;
+}
+
+.chat-suggestions-label {
+ display: block;
+ font-size: 10px;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-muted);
+ margin-bottom: 5px;
+}
+
+.chat-suggestion-list {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.chat-suggestion-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+.chat-suggestion-badge {
+ display: inline-block;
+ border-radius: 3px;
+ padding: 1px 5px;
+ font-size: 9px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: #fff;
+ flex-shrink: 0;
+}
+
+.chat-suggestion-excerpt {
+ font-size: 11px;
+ color: var(--text-muted);
+ font-style: italic;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ flex: 1;
+ min-width: 0;
+}
+
+.chat-suggestion-applied {
+ display: inline-block;
+ font-size: 9px;
+ font-weight: 700;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ color: #1a1208;
+ background: rgba(30, 200, 120, 0.85);
+ border-radius: 3px;
+ padding: 1px 5px;
+ flex-shrink: 0;
+}
diff --git a/src/renderer/components/AIChat/ChatMessageItem.tsx b/src/renderer/components/AIChat/ChatMessageItem.tsx
index ccae418..8afcaf7 100644
--- a/src/renderer/components/AIChat/ChatMessageItem.tsx
+++ b/src/renderer/components/AIChat/ChatMessageItem.tsx
@@ -1,7 +1,7 @@
import { useMemo } from 'react'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
-import type { ChatMessage } from '../../types/editor'
+import type { ChatMessage, TextAnnotation } from '../../types/editor'
marked.setOptions({ breaks: true })
@@ -11,11 +11,22 @@ function attachmentIcon(mimeType: string): string {
return '📝'
}
-interface Props {
- message: ChatMessage
+function badgeColor(type: TextAnnotation['type']): string {
+ switch (type) {
+ case 'passive_voice': return 'rgba(255, 200, 0, 0.75)'
+ case 'consistency': return 'rgba(220, 80, 80, 0.75)'
+ case 'style': return 'rgba(80, 160, 255, 0.75)'
+ case 'critique': return 'rgba(160, 80, 220, 0.75)'
+ case 'custom': return 'rgba(30, 200, 150, 0.8)'
+ }
}
-export function ChatMessageItem({ message }: Props): JSX.Element {
+interface Props {
+ message: ChatMessage
+ linkedAnnotations?: TextAnnotation[]
+}
+
+export function ChatMessageItem({ message, linkedAnnotations }: Props): JSX.Element {
const html = useMemo(() => {
if (message.role !== 'assistant' || !message.content) return null
const raw = marked.parse(message.content) as string
@@ -47,6 +58,34 @@ export function ChatMessageItem({ message }: Props): JSX.Element {
message.content || ▍
)}
+
+ {linkedAnnotations && linkedAnnotations.length > 0 && (
+
@@ -148,9 +153,18 @@ export function ChatPanel(): JSX.Element {
Ask anything about the current chapter — passive voice, plot, character, style...
)}
- {chatHistory.map((msg) => (
-
- ))}
+ {chatHistory.map((msg) => {
+ const linkedAnnotations = (msg.annotationIds ?? [])
+ .map(id => allAnnotationsForFile.find(a => a.id === id))
+ .filter((a): a is TextAnnotation => a !== undefined)
+ return (
+
+ )
+ })}
{isAILoading && chatHistory[chatHistory.length - 1]?.content === '' && (
diff --git a/src/renderer/components/Editor/MarkdownEditor.tsx b/src/renderer/components/Editor/MarkdownEditor.tsx
index bc84edf..877117a 100644
--- a/src/renderer/components/Editor/MarkdownEditor.tsx
+++ b/src/renderer/components/Editor/MarkdownEditor.tsx
@@ -155,11 +155,11 @@ export function scrollToAnnotation(ann: TextAnnotation): void {
})
}
-// Apply a suggestion to the document and remove that annotation
+// Apply a suggestion to the document and mark that annotation as applied
export function applyAnnotation(ann: TextAnnotation, suggestion: string): void {
const view = currentEditorView
if (!view) return
- const { annotations: anns, setAnnotations } = useEditorStore.getState()
+ const { annotations: anns, markAnnotationApplied } = useEditorStore.getState()
const changeSpec = { from: ann.from, to: ann.to, insert: suggestion }
// Map surviving annotation positions through the text change so
// their from/to reflect the new document offsets.
@@ -173,7 +173,9 @@ export function applyAnnotation(ann: TextAnnotation, suggestion: string): void {
changes: changeSpec,
effects: setAnnotationsEffect.of(remaining)
})
- setAnnotations(remaining)
+ // Mark as applied in annotationsByFile (preserves it for chat history) and
+ // remove from the active annotations list.
+ markAnnotationApplied(ann.id)
tooltipAnalysisCache.delete(ann.id)
}
diff --git a/src/renderer/store/editorStore.ts b/src/renderer/store/editorStore.ts
index 40ed097..40ec258 100644
--- a/src/renderer/store/editorStore.ts
+++ b/src/renderer/store/editorStore.ts
@@ -37,6 +37,8 @@ interface EditorState {
setAnnotations: (annotations: TextAnnotation[]) => void
removeAnnotation: (id: string) => void
clearAnnotations: () => void
+ markAnnotationApplied: (id: string) => void
+ linkAnnotationsToMessage: (messageId: string, annotationIds: string[]) => void
// Analysis mode
analysisMode: AnalysisMode
@@ -133,7 +135,7 @@ export const useEditorStore = create((set, get) => ({
activeFileContent: content,
isDirty: false,
chatHistory: existing,
- annotations: savedAnnotationState?.annotations ?? [],
+ annotations: savedAnnotationState?.annotations.filter(a => !a.applied) ?? [],
analysisMode: savedAnnotationState?.mode ?? 'none'
})
scheduleSave(() => {
@@ -238,8 +240,13 @@ export const useEditorStore = create((set, get) => ({
annotationsByFile: {},
setAnnotations: (annotations) => {
set((s) => {
+ // Preserve previously applied annotations so chat history links remain valid
+ const existingApplied = s.activeFilePath
+ ? (s.annotationsByFile[s.activeFilePath]?.annotations ?? []).filter(a => a.applied)
+ : []
+ const merged = [...existingApplied, ...annotations]
const annotationsByFile = s.activeFilePath
- ? { ...s.annotationsByFile, [s.activeFilePath]: { mode: s.analysisMode, annotations } }
+ ? { ...s.annotationsByFile, [s.activeFilePath]: { mode: s.analysisMode, annotations: merged } }
: s.annotationsByFile
return { annotations, annotationsByFile }
})
@@ -289,6 +296,53 @@ export const useEditorStore = create((set, get) => ({
})
},
+ markAnnotationApplied: (id) => {
+ set((s) => {
+ if (!s.activeFilePath) return {}
+ const fileState = s.annotationsByFile[s.activeFilePath]
+ if (!fileState) return {}
+ const updatedAll = fileState.annotations.map(a =>
+ a.id === id ? { ...a, applied: true } : a
+ )
+ const annotationsByFile = {
+ ...s.annotationsByFile,
+ [s.activeFilePath]: { ...fileState, annotations: updatedAll }
+ }
+ const annotations = s.annotations.filter(a => a.id !== id)
+ return { annotations, annotationsByFile }
+ })
+ scheduleSave(() => {
+ const st = get()
+ return {
+ activeFilePath: st.activeFilePath,
+ scrollPositions: st.scrollPositions,
+ chatHistoryByFile: st.chatHistoryByFile,
+ annotationsByFile: st.annotationsByFile
+ }
+ })
+ },
+
+ linkAnnotationsToMessage: (messageId, annotationIds) => {
+ set((s) => {
+ const history = s.chatHistory.map(m =>
+ m.id === messageId ? { ...m, annotationIds } : m
+ )
+ const byFile = s.activeFilePath
+ ? { ...s.chatHistoryByFile, [s.activeFilePath]: history }
+ : s.chatHistoryByFile
+ return { chatHistory: history, chatHistoryByFile: byFile }
+ })
+ scheduleSave(() => {
+ const st = get()
+ return {
+ activeFilePath: st.activeFilePath,
+ scrollPositions: st.scrollPositions,
+ chatHistoryByFile: st.chatHistoryByFile,
+ annotationsByFile: st.annotationsByFile
+ }
+ })
+ },
+
analysisMode: 'none',
setAnalysisMode: (analysisMode) => {
set((s) => {
diff --git a/src/renderer/types/editor.ts b/src/renderer/types/editor.ts
index 849f55a..9313424 100644
--- a/src/renderer/types/editor.ts
+++ b/src/renderer/types/editor.ts
@@ -19,6 +19,7 @@ export interface ChatMessage {
role: 'user' | 'assistant'
content: string
attachments?: AttachmentMeta[] // metadata only — stored in history for display
+ annotationIds?: string[] // IDs of suggestions this message produced
}
export type AnnotationType = 'passive_voice' | 'consistency' | 'style' | 'critique' | 'custom'
@@ -31,6 +32,7 @@ export interface TextAnnotation {
matchedText: string
message: string
suggestion?: string
+ applied?: boolean // true when the suggestion has been applied to the document
}
export type AnalysisMode = 'none' | 'passive_voice' | 'consistency' | 'style' | 'critique'
diff --git a/src/renderer/utils/annotationParser.ts b/src/renderer/utils/annotationParser.ts
index 8e1dd38..629ff34 100644
--- a/src/renderer/utils/annotationParser.ts
+++ b/src/renderer/utils/annotationParser.ts
@@ -10,6 +10,7 @@ export function parseAnnotationsFromAIResponse(
): TextAnnotation[] {
const annotations: TextAnnotation[] = []
let id = 0
+ const runId = Date.now()
// Match quoted strings — handles "straight", "curly", and 'single' quotes
// Minimum 10 chars to avoid matching short words
@@ -50,7 +51,7 @@ export function parseAnnotationsFromAIResponse(
const message = extractMessage(aiResponse, match.index)
annotations.push({
- id: `ai-${id++}`,
+ id: `ai-${runId}-${id++}`,
type,
from: docIndex,
to: docIndex + quotedText.length,