diff --git a/src/renderer/components/Feedback/FeedbackPanel.css b/src/renderer/components/Feedback/FeedbackPanel.css
index 81e3750..bb8e9df 100644
--- a/src/renderer/components/Feedback/FeedbackPanel.css
+++ b/src/renderer/components/Feedback/FeedbackPanel.css
@@ -252,3 +252,159 @@
.fb-card-apply:hover {
opacity: 0.85;
}
+
+/* ── Archive section ─────────────────────────────────── */
+.fb-archive {
+ flex-shrink: 0;
+ border-top: 1px solid var(--border);
+}
+
+.fb-archive-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 7px 12px;
+ cursor: pointer;
+ list-style: none;
+ user-select: none;
+ color: var(--text-muted);
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ transition: color 0.15s;
+}
+
+.fb-archive-header::-webkit-details-marker { display: none; }
+
+.fb-archive-header:hover {
+ color: var(--text-secondary);
+}
+
+.fb-archive-title {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.fb-archive-count {
+ background: var(--border);
+ color: var(--text-muted);
+ border-radius: 10px;
+ font-size: 10px;
+ font-weight: 700;
+ padding: 1px 6px;
+ letter-spacing: 0;
+}
+
+.fb-archive-clear-btn {
+ background: none;
+ border: 1px solid var(--border);
+ border-radius: 4px;
+ color: var(--text-muted);
+ font-size: 10px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ padding: 2px 7px;
+ cursor: pointer;
+ transition: color 0.15s, border-color 0.15s;
+}
+
+.fb-archive-clear-btn:hover {
+ color: #e07070;
+ border-color: rgba(200, 60, 60, 0.5);
+}
+
+.fb-archive-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 6px 12px 10px;
+ max-height: 260px;
+ overflow-y: auto;
+}
+
+/* ── Archive card ────────────────────────────────────── */
+.fb-archive-card {
+ flex-shrink: 0;
+ border: 1px solid var(--border);
+ border-radius: 5px;
+ padding: 7px 10px;
+ background: var(--message-bg);
+ opacity: 0.7;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.fb-archive-card:hover {
+ opacity: 1;
+}
+
+.fb-archive-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+}
+
+.fb-archive-card-badges {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex-wrap: wrap;
+}
+
+/* "applied" / "dismissed" status tag */
+.fb-archive-status {
+ font-size: 9px;
+ font-weight: 600;
+ letter-spacing: 0.06em;
+ text-transform: uppercase;
+ border-radius: 3px;
+ padding: 1px 5px;
+ white-space: nowrap;
+ align-self: flex-start;
+}
+
+.fb-archive-status--applied {
+ color: rgba(80, 200, 120, 0.85);
+ border: 1px solid rgba(80, 200, 120, 0.3);
+}
+
+.fb-archive-status--dismissed {
+ color: rgba(160, 160, 160, 0.75);
+ border: 1px solid rgba(160, 160, 160, 0.25);
+}
+
+.fb-archive-card-dismiss {
+ background: none;
+ border: none;
+ color: var(--text-muted);
+ font-size: 15px;
+ line-height: 1;
+ padding: 0 2px;
+ cursor: pointer;
+ opacity: 0;
+ transition: opacity 0.15s, color 0.15s;
+ flex-shrink: 0;
+}
+
+.fb-archive-card:hover .fb-archive-card-dismiss {
+ opacity: 1;
+}
+
+.fb-archive-card-dismiss:hover {
+ color: var(--text-primary);
+}
+
+.fb-archive-card-excerpt {
+ font-size: 11px;
+ color: var(--text-muted);
+ font-style: italic;
+ line-height: 1.5;
+}
+
+.fb-archive-card-suggestion {
+ font-size: 11px;
+ color: var(--text-secondary);
+ line-height: 1.5;
+}
diff --git a/src/renderer/components/Feedback/FeedbackPanel.tsx b/src/renderer/components/Feedback/FeedbackPanel.tsx
index 723328c..4664d87 100644
--- a/src/renderer/components/Feedback/FeedbackPanel.tsx
+++ b/src/renderer/components/Feedback/FeedbackPanel.tsx
@@ -145,10 +145,56 @@ function FeedbackCard({ ann, autoAnalyse, onDismiss }: FeedbackCardProps): JSX.E
)
}
+interface ArchiveCardProps {
+ ann: TextAnnotation
+ onRemove?: () => void
+}
+
+function ArchiveCard({ ann, onRemove }: ArchiveCardProps): JSX.Element {
+ const typeName = ann.type.replace(/_/g, ' ')
+ const status = ann.applied ? 'applied' : 'dismissed'
+
+ return (
+
+
+
+ {typeName}
+ {status}
+
+ {onRemove && (
+
+ )}
+
+
"{ann.matchedText}"
+ {ann.suggestion && (
+
→ {ann.suggestion}
+ )}
+
+ )
+}
+
export function FeedbackPanel(): JSX.Element {
- const { annotations, clearAnnotations, removeAnnotation } = useEditorStore()
+ const {
+ annotations,
+ annotationsByFile,
+ activeFilePath,
+ clearAnnotations,
+ removeAnnotation,
+ clearArchivedAnnotations,
+ removeArchivedAnnotation,
+ } = useEditorStore()
const [analyseAll, setAnalyseAll] = useState(false)
+ const archivedAnnotations = activeFilePath
+ ? (annotationsByFile[activeFilePath]?.annotations ?? []).filter(a => a.applied || a.dismissed)
+ : []
+ const dismissedCount = archivedAnnotations.filter(a => a.dismissed).length
+
// Reset "Analyse all" whenever the annotation set changes (new critique run),
// so auto-analysis doesn't carry over to fresh results unexpectedly.
const prevAnnotationsRef = useRef(annotations)
@@ -164,7 +210,10 @@ export function FeedbackPanel(): JSX.Element {
tooltipAnalysisCache.clear()
}
- if (annotations.length === 0) {
+ const hasActive = annotations.length > 0
+ const hasArchive = archivedAnnotations.length > 0
+
+ if (!hasActive && !hasArchive) {
return (
@@ -177,34 +226,67 @@ export function FeedbackPanel(): JSX.Element {
return (
-
-
-
-
+ {hasActive && (
+ <>
+
+
+
+
-
- {annotations.map(ann => (
- { cancelPendingDismiss(ann.id); removeAnnotation(ann.id); tooltipAnalysisCache.delete(ann.id) }}
- />
- ))}
-
+
+ {annotations.map(ann => (
+ { cancelPendingDismiss(ann.id); removeAnnotation(ann.id); tooltipAnalysisCache.delete(ann.id) }}
+ />
+ ))}
+
+ >
+ )}
+
+ {hasArchive && (
+
+
+
+ Archive
+ {archivedAnnotations.length}
+
+ {dismissedCount > 0 && (
+
+ )}
+
+
+ {archivedAnnotations.map(ann => (
+
removeArchivedAnnotation(ann.id) : undefined}
+ />
+ ))}
+
+
+ )}
)
}
diff --git a/src/renderer/store/editorStore.ts b/src/renderer/store/editorStore.ts
index f7c2541..a2b8d18 100644
--- a/src/renderer/store/editorStore.ts
+++ b/src/renderer/store/editorStore.ts
@@ -41,6 +41,8 @@ interface EditorState {
clearAnnotations: () => void
markAnnotationApplied: (id: string) => void
linkAnnotationsToMessage: (messageId: string, annotationIds: string[]) => void
+ clearArchivedAnnotations: () => void
+ removeArchivedAnnotation: (id: string) => void
// Analysis mode
analysisMode: AnalysisMode
@@ -140,7 +142,7 @@ export const useEditorStore = create
((set, get) => ({
activeFileContent: content,
isDirty: false,
chatHistory: existing,
- annotations: savedAnnotationState?.annotations.filter(a => !a.applied) ?? [],
+ annotations: savedAnnotationState?.annotations.filter(a => !a.applied && !a.dismissed) ?? [],
analysisMode: savedAnnotationState?.mode ?? 'none'
})
scheduleSave(() => {
@@ -282,11 +284,11 @@ 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)
+ // Preserve applied and dismissed annotations so chat history links and archive remain intact
+ const existingArchived = s.activeFilePath
+ ? (s.annotationsByFile[s.activeFilePath]?.annotations ?? []).filter(a => a.applied || a.dismissed)
: []
- const merged = [...existingApplied, ...annotations]
+ const merged = [...existingArchived, ...annotations]
const annotationsByFile = s.activeFilePath
? { ...s.annotationsByFile, [s.activeFilePath]: { mode: s.analysisMode, annotations: merged } }
: s.annotationsByFile
@@ -306,8 +308,12 @@ export const useEditorStore = create((set, get) => ({
removeAnnotation: (id) => {
set((s) => {
const annotations = s.annotations.filter((a) => a.id !== id)
+ const fileAnnotations = s.activeFilePath
+ ? (s.annotationsByFile[s.activeFilePath]?.annotations ?? [])
+ : []
+ const updatedAll = fileAnnotations.map(a => a.id === id ? { ...a, dismissed: true } : a)
const annotationsByFile = s.activeFilePath
- ? { ...s.annotationsByFile, [s.activeFilePath]: { mode: s.analysisMode, annotations } }
+ ? { ...s.annotationsByFile, [s.activeFilePath]: { mode: s.analysisMode, annotations: updatedAll } }
: s.annotationsByFile
return { annotations, annotationsByFile }
})
@@ -324,9 +330,16 @@ export const useEditorStore = create((set, get) => ({
},
clearAnnotations: () => {
set((s) => {
- const annotationsByFile = s.activeFilePath
- ? { ...s.annotationsByFile, [s.activeFilePath]: { mode: 'none' as AnalysisMode, annotations: [] } }
- : s.annotationsByFile
+ if (!s.activeFilePath) return { annotations: [], analysisMode: 'none' as AnalysisMode }
+ const fileState = s.annotationsByFile[s.activeFilePath]
+ // Archive active annotations as dismissed; leave already-archived items untouched
+ const updatedAll = (fileState?.annotations ?? []).map(a =>
+ !a.applied && !a.dismissed ? { ...a, dismissed: true } : a
+ )
+ const annotationsByFile = {
+ ...s.annotationsByFile,
+ [s.activeFilePath]: { mode: 'none' as AnalysisMode, annotations: updatedAll }
+ }
return { annotations: [], analysisMode: 'none', annotationsByFile }
})
scheduleSave(() => {
@@ -392,6 +405,54 @@ export const useEditorStore = create((set, get) => ({
})
},
+ clearArchivedAnnotations: () => {
+ set((s) => {
+ if (!s.activeFilePath) return {}
+ const fileState = s.annotationsByFile[s.activeFilePath]
+ if (!fileState) return {}
+ return {
+ annotationsByFile: {
+ ...s.annotationsByFile,
+ [s.activeFilePath]: { ...fileState, annotations: fileState.annotations.filter(a => !a.dismissed) }
+ }
+ }
+ })
+ scheduleSave(() => {
+ const st = get()
+ return {
+ activeFilePath: st.activeFilePath,
+ scrollPositions: st.scrollPositions,
+ chatSessionsByFile: st.chatSessionsByFile,
+ activeSessionIdByFile: st.activeSessionIdByFile,
+ annotationsByFile: st.annotationsByFile
+ }
+ })
+ },
+
+ removeArchivedAnnotation: (id) => {
+ set((s) => {
+ if (!s.activeFilePath) return {}
+ const fileState = s.annotationsByFile[s.activeFilePath]
+ if (!fileState) return {}
+ return {
+ annotationsByFile: {
+ ...s.annotationsByFile,
+ [s.activeFilePath]: { ...fileState, annotations: fileState.annotations.filter(a => a.id !== id) }
+ }
+ }
+ })
+ scheduleSave(() => {
+ const st = get()
+ return {
+ activeFilePath: st.activeFilePath,
+ scrollPositions: st.scrollPositions,
+ chatSessionsByFile: st.chatSessionsByFile,
+ activeSessionIdByFile: st.activeSessionIdByFile,
+ annotationsByFile: st.annotationsByFile
+ }
+ })
+ },
+
analysisMode: 'none',
setAnalysisMode: (analysisMode) => {
set((s) => {
diff --git a/src/renderer/types/editor.ts b/src/renderer/types/editor.ts
index 4a4e575..be8d689 100644
--- a/src/renderer/types/editor.ts
+++ b/src/renderer/types/editor.ts
@@ -39,6 +39,7 @@ export interface TextAnnotation {
message: string
suggestion?: string
applied?: boolean // true when the suggestion has been applied to the document
+ dismissed?: boolean // true when the user dismissed this annotation (archived)
}
export type AnalysisMode = 'none' | 'passive_voice' | 'consistency' | 'style' | 'critique'