Skip to content

feat(feedback): persistent, resumable Send Feedback drafts (#756)#1133

Open
jSydorowicz21 wants to merge 4 commits into
rcfrom
triage/issue-756-feedback-drafts
Open

feat(feedback): persistent, resumable Send Feedback drafts (#756)#1133
jSydorowicz21 wants to merge 4 commits into
rcfrom
triage/issue-756-feedback-drafts

Conversation

@jSydorowicz21

@jSydorowicz21 jSydorowicz21 commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #756. Send Feedback drafts now persist across modal/app close and become a browsable, resumable list with metadata and attachments. Previously a draft was only ephemeral in-memory React state.

Changes

  • New feedback:drafts:list|save|delete IPC handlers backed by a userData JSON file (mirrors the playbooks persistence pattern: crypto.randomUUID ids, createdAt/updatedAt, upsert-by-id, capped, sorted by updatedAt desc).
  • All four window.maestro.feedback lockstep touch-points updated: preload FeedbackApi, the inline global.d.ts literal, the store, and the test setup mock.
  • feedbackDraftStore extended with loadDrafts/saveDraft/deleteDraft/resume tracking; reset() now clears only ephemeral session state and preserves the drafts list.
  • New FeedbackDraftsList overlay (suggested name, created/updated times, attachment indicator, resume/delete), a Drafts header button with count badge in FeedbackModal, resume wiring, and a save-or-discard close dialog.

Verification

  • npm run lint (tsc x3): clean
  • vitest run: IPC handler, preload, store, and view tests: 38 tests pass
  • eslint + prettier: clean

Summary by CodeRabbit

  • New Features
    • Added saved feedback drafts with resume, edit, and delete capabilities in the feedback modal.
    • Introduced a drafts sidebar to view recent drafts (including attachments) and quickly resume or remove them.
    • Added draft persistence with “Save draft” and automatic snapshotting during minimize/close flows.
  • Bug Fixes
    • Improved draft recovery by normalizing and clamping saved content and restoring last-response readiness state.
    • Added robust handling for concurrent saves to prevent duplicate or lost drafts.
  • Tests
    • Expanded coverage for IPC, preload API, store logic, and UI draft-resume behavior.

Feedback drafts now persist to a userData JSON file (mirroring the playbooks store) so they survive modal/app close. Adds drafts list/save/delete IPC + preload + store, a FeedbackDraftsList overlay with metadata and attachment indicators, resume wiring, and a save-or-discard close dialog. Covered by IPC, preload, store, and view tests.
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a7448234-5b89-425e-a858-3322e1540bf8

📥 Commits

Reviewing files that changed from the base of the PR and between c270acb and 59df6b7.

📒 Files selected for processing (2)
  • src/__tests__/renderer/stores/feedbackDraftStore.test.ts
  • src/renderer/stores/feedbackDraftStore.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/tests/renderer/stores/feedbackDraftStore.test.ts
  • src/renderer/stores/feedbackDraftStore.ts

📝 Walkthrough

Walkthrough

Feedback drafts are now persisted in the main process, exposed through preload and renderer APIs, and surfaced in the feedback modal and chat view for save, resume, list, and delete actions.

Changes

Feedback drafts

Layer / File(s) Summary
Main drafts persistence
src/main/ipc/handlers/feedback.ts, src/__tests__/main/ipc/handlers/feedback.test.ts
Draft records, file-backed persistence helpers, and list/save/delete IPC handlers are added in the main process, with tests covering registration and normalized persistence behavior.
Preload drafts API
src/main/preload/feedback.ts, src/renderer/global.d.ts, src/__tests__/main/preload/feedback.test.ts, src/__tests__/setup.ts
Draft interfaces and the feedback.drafts preload bridge are added, with renderer typings and IPC-call tests for list/save/delete.
Draft store state
src/renderer/stores/feedbackDraftStore.ts, src/__tests__/renderer/stores/feedbackDraftStore.test.ts
The feedback draft store now tracks persisted drafts, active/resume state, and save errors, and tests cover load/save/delete/reset behavior.
Chat resume and save
src/renderer/components/FeedbackChatView.tsx, src/__tests__/renderer/components/FeedbackChatView.test.tsx
The feedback chat view hydrates from resumeDraftId, serializes the current draft, saves it through the store, and tests cover resume hydration, fallback provider selection, save payloads, and submit-ready restoration.
Drafts sidebar and modal
src/renderer/components/FeedbackModal.tsx, src/renderer/components/FeedbackDraftsList.tsx, src/renderer/hooks/modal/useModalHandlers.ts, src/__tests__/renderer/components/FeedbackModal.test.tsx
The modal loads drafts on open, shows the saved-drafts sidebar, saves before minimize/close/resume actions, and uses the new drafts list component; tests cover switching and no-op resume behavior.

Sequence Diagram(s)

sequenceDiagram
  participant useModalHandlers
  participant FeedbackModal
  participant useFeedbackDraftStore
  participant DraftsApi as "window.maestro.feedback.drafts"
  participant FeedbackHandlers as "src/main/ipc/handlers/feedback.ts"
  participant FeedbackChatView
  participant FeedbackDraftsList

  useModalHandlers->>FeedbackModal: open feedback modal
  FeedbackModal->>useFeedbackDraftStore: loadDrafts()
  useFeedbackDraftStore->>DraftsApi: list()
  DraftsApi->>FeedbackHandlers: feedback:drafts:list
  FeedbackHandlers-->>DraftsApi: drafts[]
  DraftsApi-->>useFeedbackDraftStore: drafts[]
  FeedbackModal->>FeedbackDraftsList: render saved drafts
  FeedbackDraftsList-->>FeedbackModal: onResume(draft.id) / onDelete(draft.id)
  FeedbackModal->>useFeedbackDraftStore: requestResume(draft.id)
  FeedbackModal->>FeedbackChatView: resumeDraftId
  FeedbackChatView->>useFeedbackDraftStore: saveDraft()
  useFeedbackDraftStore->>DraftsApi: save(draft)
  DraftsApi->>FeedbackHandlers: feedback:drafts:save
  FeedbackHandlers-->>DraftsApi: draft
  DraftsApi-->>useFeedbackDraftStore: draft
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • RunMaestro/Maestro#491: Extends the same feedback IPC/preload module, but for feedback auth/submit flows rather than draft persistence and resume behavior.

Poem

A bunny tucked a draft away,
Then hopped back later, bright and gray.
With saved notes, clips, and tiny signs,
The feedback trail now neatly shines. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: persistent, resumable feedback drafts.
Linked Issues check ✅ Passed The changes add draft persistence, resume/delete flows, metadata, attachments, and a most-recent list as requested.
Out of Scope Changes check ✅ Passed The diffs stay focused on feedback draft persistence, UI, store, preload, tests, and typings.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch triage/issue-756-feedback-drafts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds persistent feedback drafts to the Send Feedback flow. The main changes are:

  • New draft list, save, and delete IPC handlers backed by a userData JSON file.
  • Draft loading, saving, deletion, resume state, and save error handling in the renderer store.
  • Draft resume UI, close/save prompts, and minimize persistence in the feedback modal.
  • Draft hydration, manual save, provider fallback, and submit-ready state restoration in the chat view.
  • Updated preload types, global types, mocks, and tests for the draft API.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
src/main/ipc/handlers/feedback.ts Adds persisted draft normalization plus serialized list, save, and delete handlers.
src/main/preload/feedback.ts Exposes the feedback drafts API through the preload bridge.
src/renderer/stores/feedbackDraftStore.ts Adds persisted draft state, resume tracking, save errors, and concurrent first-save handling.
src/renderer/components/FeedbackModal.tsx Adds draft browsing, resume wiring, minimize persistence, and save-or-discard close behavior.
src/renderer/components/FeedbackChatView.tsx Adds draft hydration, live draft snapshots, manual saving, and restored submit-ready state.

Reviews (3): Last reviewed commit: "fix(feedback): dedupe concurrent first-s..." | Re-trigger Greptile

Comment thread src/renderer/components/FeedbackModal.tsx Outdated
Comment thread src/renderer/components/FeedbackModal.tsx
Comment thread src/renderer/components/FeedbackModal.tsx Outdated
Comment thread src/renderer/components/FeedbackChatView.tsx
Comment thread src/main/ipc/handlers/feedback.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e647e3ada0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +571 to +574
category: lastResponse?.category ?? 'general_feedback',
summary: lastResponse?.summary ?? '',
confidence,
agentType: selectedAgent,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist the ready response when saving drafts

When a user saves after the assistant has reached the submit-ready state, this draft only keeps the category, summary, and confidence while dropping lastResponse.structured and the ready flag that createNewIssue() requires. On resume, confidence can show 80% or higher, but isReady is false and lastResponse is null, so the Submit Feedback button stays hidden until the user sends another message. Persist and hydrate the parsed response, or otherwise reconstruct it, so a ready draft remains submittable after resume.

Useful? React with 👍 / 👎.

// Auto-select first available
const firstAvailable = AGENT_TILES.find((t) => t.supported && available.has(t.id));
if (firstAvailable) setSelectedAgent(firstAvailable.id);
if (firstAvailable && !resumeDraft) setSelectedAgent(firstAvailable.id);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back when a draft's provider is unavailable

When resuming a draft created with a provider that is no longer available, this condition keeps selectedAgent set to the saved provider as long as any supported provider exists. The auto-start gate only checks availableTiles.length, and sendMessage() later throws if the selected agent is unavailable, with no UI left for the user to switch providers. Fall back to firstAvailable when the saved agentType is not in the detected set.

Useful? React with 👍 / 👎.

try {
const { draft: saved } = await window.maestro.feedback.drafts.save(draft);
await get().loadDrafts();
set({ activeDraftId: saved.id });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh the active snapshot after saving

When a new draft is saved, the IPC handler returns the generated id, but the store only updates activeDraftId and leaves activeDraft with its old empty id. The close and minimize paths later call saveDraft(activeDraft), so if the user clicks Save draft and then Save & Close or minimize without editing, the stale empty id is sent and the main handler creates a duplicate draft instead of updating the saved one. Patch the active snapshot with saved.id when saving succeeds.

Useful? React with 👍 / 👎.

Comment on lines +93 to +94
} catch {
return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Surface draft save failures before reporting success

If the IPC save fails because disk persistence throws or the bridge rejects, this catch converts the failure to null; callers do not check for null, so the Save draft button still shows Saved and Save & Close still closes the modal, leaving users thinking their feedback is persisted when it was not. Let the error propagate or return a failure state that callers must handle before showing success or closing.

Useful? React with 👍 / 👎.

Comment on lines +180 to +183
const handleResume = useCallback((id: string) => {
setShowDrafts(false);
useFeedbackDraftStore.getState().requestResume(id);
setResumeNonce((n) => n + 1);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Prompt before replacing an unsaved feedback draft

When the editor already contains unsaved feedback, clicking a saved draft immediately requests the resume and bumps the key, which unmounts the current FeedbackChatView and runs its reset cleanup without saving or asking about the live activeDraft. A user who opens the Drafts list while composing a new unsaved report loses that in-progress text as soon as they resume another draft, so this path should save or confirm before switching drafts.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/main/ipc/handlers/feedback.ts (2)

208-216: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

list returns on-disk drafts without normalization.

readDrafts() casts data.drafts straight to FeedbackDraft[], so feedback:drafts:list hands the renderer whatever is on disk, whereas save always normalizes. If the file is ever from an older schema or hand-edited, malformed drafts reach the UI untyped. Mapping read results through normalizeDraft keeps the read/write contract symmetric.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/ipc/handlers/feedback.ts` around lines 208 - 216, readDrafts() is
returning raw on-disk draft objects instead of normalized FeedbackDrafts, so
feedback:drafts:list can surface malformed legacy or hand-edited data. Update
readDrafts() in src/main/ipc/handlers/feedback.ts to map any parsed data.drafts
entries through normalizeDraft before returning them, keeping the read path
symmetric with save() and ensuring the renderer only receives normalized drafts.

13-13: 📐 Maintainability & Code Quality | 🔵 Trivial

Use the shared UUID helper instead of crypto.randomUUID

Per coding guidelines, ID generation should use generateUUID() from src/shared/uuid.ts to avoid duplicate implementations.

♻️ Suggested change
- import { randomUUID } from 'crypto';
+ import { generateUUID } from '../../../shared/uuid';
-		id: typeof d.id === 'string' && d.id ? d.id : randomUUID(),
+		id: typeof d.id === 'string' && d.id ? d.id : generateUUID(),

This applies to line 13 and line 295 where randomUUID() is used.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/ipc/handlers/feedback.ts` at line 13, Replace the direct UUID
generation in feedback.ts with the shared generateUUID() helper from
src/shared/uuid.ts. Update the import at the top of the file and change both
places that currently call randomUUID() so they use generateUUID() instead,
keeping the existing logic in the feedback handler functions unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/ipc/handlers/feedback.ts`:
- Around line 1216-1254: The draft persistence handlers in feedback:drafts:save
and feedback:drafts:delete are doing an unguarded read-modify-write cycle that
can interleave and overwrite concurrent changes. Add a shared in-memory
serialization mechanism around the draft operations in
src/main/ipc/handlers/feedback.ts, such as a single promise chain or mutex used
by both ipcMain.handle callbacks. Ensure readDrafts(), the mutation logic, and
writeDrafts() run atomically per request so overlapping saves/deletes cannot
clobber each other.

In `@src/renderer/components/FeedbackChatView.tsx`:
- Around line 594-600: The draft snapshot is using a stale or missing draft id
because `serializeDraft` and the `useEffect` that calls
`useFeedbackDraftStore.getState().setActiveDraft(...)` do not depend on
`activeDraftId`. Update both dependency lists so `FeedbackChatView` re-runs the
serialization whenever `activeDraftId` changes, ensuring the snapshot always
carries the current identifier. This should be fixed in the `serializeDraft`
logic and the surrounding snapshot effect that reads `messages`, `attachments`,
and `inputValue`.

---

Nitpick comments:
In `@src/main/ipc/handlers/feedback.ts`:
- Around line 208-216: readDrafts() is returning raw on-disk draft objects
instead of normalized FeedbackDrafts, so feedback:drafts:list can surface
malformed legacy or hand-edited data. Update readDrafts() in
src/main/ipc/handlers/feedback.ts to map any parsed data.drafts entries through
normalizeDraft before returning them, keeping the read path symmetric with
save() and ensuring the renderer only receives normalized drafts.
- Line 13: Replace the direct UUID generation in feedback.ts with the shared
generateUUID() helper from src/shared/uuid.ts. Update the import at the top of
the file and change both places that currently call randomUUID() so they use
generateUUID() instead, keeping the existing logic in the feedback handler
functions unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 39c8f3f5-b7d4-4b71-bead-cdb746b6c8e4

📥 Commits

Reviewing files that changed from the base of the PR and between 9972eee and e647e3a.

📒 Files selected for processing (13)
  • src/__tests__/main/ipc/handlers/feedback.test.ts
  • src/__tests__/main/preload/feedback.test.ts
  • src/__tests__/renderer/components/FeedbackChatView.test.tsx
  • src/__tests__/renderer/stores/feedbackDraftStore.test.ts
  • src/__tests__/setup.ts
  • src/main/ipc/handlers/feedback.ts
  • src/main/preload/feedback.ts
  • src/renderer/components/FeedbackChatView.tsx
  • src/renderer/components/FeedbackDraftsList.tsx
  • src/renderer/components/FeedbackModal.tsx
  • src/renderer/global.d.ts
  • src/renderer/hooks/modal/useModalHandlers.ts
  • src/renderer/stores/feedbackDraftStore.ts

Comment thread src/main/ipc/handlers/feedback.ts Outdated
Comment thread src/renderer/components/FeedbackChatView.tsx
Addresses all review findings: discard no longer deletes a resumed draft; failed saves keep the modal open and surface a save-error banner instead of silently closing/minimizing; the minted draft id is propagated back to the live snapshot so save+minimize no longer duplicates; concurrent draft writes are serialized via the shared atomic-json-store keyed write queue (mirrors group-chat-storage); resumed drafts restore the submit-ready response and fall back to an available provider when the saved one is gone; switching to another draft auto-saves the current one first.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 43ff3e7f3c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// Preserve any unsaved in-progress editor before we unmount it to resume a
// different draft, so switching drafts never silently discards work.
const { activeDraft, activeDraftId, saveDraft } = useFeedbackDraftStore.getState();
if (activeDraft && activeDraftId !== id) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve edits when reopening the current draft

When the editor is already showing draft A and the user has typed unsaved changes, opening Saved Drafts and clicking draft A again skips saveDraft because activeDraftId === id, then setResumeNonce remounts FeedbackChatView from the persisted copy. That drops the unsaved edits to the same draft. Treat this case as a no-op or save before remounting.

Useful? React with 👍 / 👎.

Comment thread src/main/ipc/handlers/feedback.ts Outdated
Comment on lines +369 to +371
inputDraft:
typeof d.inputDraft === 'string'
? sanitizeTextInput(d.inputDraft).slice(0, MAX_FEEDBACK_FIELD_LENGTH)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve long unsent draft input

When a user writes a long report in the composer and saves or minimizes before sending it, inputDraft is silently capped at MAX_FEEDBACK_FIELD_LENGTH (5000 chars), while the chat input itself has no matching limit and sent messages are preserved up to 20000 chars. On resume, everything after 5000 chars is gone with no warning. Use the draft message limit here or enforce the same cap in the textarea before saving.

Useful? React with 👍 / 👎.

Comment thread src/main/ipc/handlers/feedback.ts Outdated
Comment on lines +346 to +350
const messages = Array.isArray(d.messages)
? (d.messages
.map(normalizeDraftMessage)
.filter((m): m is FeedbackDraftMessage => m !== null)
.slice(0, MAX_DRAFT_MESSAGES) as FeedbackDraftMessage[])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep newest draft messages when trimming

When a feedback conversation grows past 200 messages, this cap keeps the oldest messages and drops the newest ones. Saving or minimizing at that point means the resumed draft loses the latest user details and assistant questions, which are the most relevant context for continuing or submitting the report. Trim from the front instead, for example by keeping the last MAX_DRAFT_MESSAGES entries.

Useful? React with 👍 / 👎.

Round-2 review fixes: trim drafts from the front so the NEWEST messages survive the 200-message cap (was keeping oldest); raise the persisted composer inputDraft cap from 5000 to the 20000 draft-message limit so long unsent reports are not silently truncated on resume; reopening the already-active draft is now a no-op (no remount) so unsaved edits are not dropped. +4 tests.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/__tests__/renderer/components/FeedbackModal.test.tsx`:
- Around line 14-37: The mocked feedback draft store is not propagating resume
state, so `FeedbackModal` tests can false-pass when `requestResume` is only a
spy. Update the `useFeedbackDraftStore` mock in `FeedbackModal.test.tsx` so
`requestResume` mutates `mockState.resumeDraftId` (and any related active/resume
fields needed by `FeedbackModal` and `FeedbackChatView`), then verify the
remounted chat receives the resumed draft id. Apply the same mock/state behavior
consistently across the affected test cases so the resume flow is actually
exercised.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1ea0bd7b-2a05-423f-813e-8186207a568f

📥 Commits

Reviewing files that changed from the base of the PR and between e647e3a and c270acb.

📒 Files selected for processing (10)
  • src/__tests__/main/ipc/handlers/feedback.test.ts
  • src/__tests__/renderer/components/FeedbackChatView.test.tsx
  • src/__tests__/renderer/components/FeedbackModal.test.tsx
  • src/__tests__/renderer/stores/feedbackDraftStore.test.ts
  • src/main/ipc/handlers/feedback.ts
  • src/main/preload/feedback.ts
  • src/renderer/components/FeedbackChatView.tsx
  • src/renderer/components/FeedbackModal.tsx
  • src/renderer/global.d.ts
  • src/renderer/stores/feedbackDraftStore.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/renderer/global.d.ts
  • src/main/preload/feedback.ts
  • src/main/ipc/handlers/feedback.ts
  • src/renderer/components/FeedbackChatView.tsx
  • src/renderer/components/FeedbackModal.tsx
  • src/renderer/stores/feedbackDraftStore.ts

Comment on lines +14 to +37
const mockState = {
isMinimized: false,
hasDraft: false,
drafts: [] as FeedbackDraft[],
resumeDraftId: null as string | null,
activeDraftId: null as string | null,
activeDraft: null as FeedbackDraft | null,
setMinimized: vi.fn(),
setHasDraft: vi.fn(),
setActiveDraft: vi.fn(),
loadDrafts: vi.fn().mockResolvedValue(undefined),
saveDraft: vi.fn().mockResolvedValue('saved-id'),
deleteDraft: vi.fn().mockResolvedValue(undefined),
requestResume: vi.fn(),
clearResume: vi.fn(),
reset: vi.fn(),
};

vi.mock('../../../renderer/stores/feedbackDraftStore', () => ({
useFeedbackDraftStore: Object.assign(
(selector: (s: typeof mockState) => unknown) => selector(mockState),
{ getState: () => mockState }
),
}));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the mocked draft store propagate resumeDraftId.

requestResume is only a spy here, so clicking resume-draft-2 never changes mockState.resumeDraftId. Because FeedbackModal also bumps resumeNonce, this test can still observe a remount even if the resumed draft id is never forwarded to FeedbackChatView. Update the mock to mutate resumeDraftId and assert the remounted chat receives that id, otherwise this scenario can false-pass while the actual resume flow is broken.

Suggested test mock fix
 const mockState = {
 	...
-	requestResume: vi.fn(),
+	requestResume: vi.fn((id: string) => {
+		mockState.resumeDraftId = id;
+	}),
 	...
 };

 vi.mock('../../../renderer/components/FeedbackChatView', () => ({
-	FeedbackChatView: () => {
+	FeedbackChatView: ({ resumeDraftId }: { resumeDraftId?: string | null }) => {
 		React.useEffect(() => {
 			mockChat.mountCount += 1;
 		}, []);
-		return <div data-testid="chat-view" />;
+		return <div data-testid="chat-view" data-resume-draft-id={resumeDraftId ?? ''} />;
 	},
 }));

Also applies to: 77-84, 179-198

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/__tests__/renderer/components/FeedbackModal.test.tsx` around lines 14 -
37, The mocked feedback draft store is not propagating resume state, so
`FeedbackModal` tests can false-pass when `requestResume` is only a spy. Update
the `useFeedbackDraftStore` mock in `FeedbackModal.test.tsx` so `requestResume`
mutates `mockState.resumeDraftId` (and any related active/resume fields needed
by `FeedbackModal` and `FeedbackChatView`), then verify the remounted chat
receives the resumed draft id. Apply the same mock/state behavior consistently
across the affected test cases so the resume flow is actually exercised.

@jSydorowicz21

Copy link
Copy Markdown
Contributor Author

@greptile re-review please

@greptile-apps

greptile-apps Bot commented Jun 26, 2026

Copy link
Copy Markdown

Want your agent to iterate on Greptile's feedback? Try greploops.

const handleSaveDraft = useCallback(async () => {
setIsSavingDraft(true);
try {
const savedId = await useFeedbackDraftStore.getState().saveDraft(serializeDraft());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Duplicate Draft Window When a fresh draft is first saved, serializeDraft() still sends id: '' until the async save returns and patches the active draft id. During that window, minimize or Save & Close can read the still-empty activeDraft from the store and issue a second save. The main handler treats each empty id as a new draft, so a quick Save draft followed by minimize or close can persist duplicate drafts instead of updating the first one. The first save should claim a stable id before it awaits IPC, or the other save paths should reuse or wait for the in-flight save.

Round-3 review fix: a brand-new draft serializes with id='' until the async save returns; the IPC handler mints a fresh UUID per empty-id call (outside the write queue), so two concurrent first-saves (e.g. Save Draft then immediate minimize/Save and Close) created duplicate rows. Serialize new-draft saves via a module-level in-flight promise: the concurrent call awaits it, reuses the minted id, and re-issues the save so the handler upserts the same row. Already-saved drafts are unaffected. +1 test.
@jSydorowicz21

Copy link
Copy Markdown
Contributor Author

@greptile re-review please

@jSydorowicz21 jSydorowicz21 added the ready to merge This PR is ready to merge label Jun 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready to merge This PR is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant