Skip to content

feat(chat): implement edit message functionality#1136

Open
fbricon wants to merge 1 commit intokortex-hub:mainfrom
fbricon:edit-button-implem
Open

feat(chat): implement edit message functionality#1136
fbricon wants to merge 1 commit intokortex-hub:mainfrom
fbricon:edit-button-implem

Conversation

@fbricon
Copy link
Contributor

@fbricon fbricon commented Mar 19, 2026

This PR provides support for editing chat messages. In the original Vercel Chatbot UI, the message was edited in place (Claude Desktop does it too), but I prefer Ollama client's approach, where editing happens in a single place, the input box at the bottom, providing a consistent UI (includes all existing and future buttons).

Clicking the edit button on a user message populates the input box with the message text, shows a "Press ESC to cancel editing" hint, and grays out all messages after the edited one. Submitting the edit deletes trailing messages from the database and regenerates the AI response. Just like in Ollama client.

  • Add EditState context for shared editing state across components
  • Add deleteTrailingMessages IPC endpoint and preload exposure
  • Update multimodal-input to handle edit mode (ESC cancel, submit edit)
  • Update preview-message to gray out messages and hide edit buttons
    during editing

Includes the fix from #1134

See it in action:

edit-message.mp4

Admittedly, there's one thing Claude Desktop does better, which is, instead of deleting trailing messages, it just forks the conversation and allows the user to switch between different branches. But it's a more complex approach that'll certainly require database schema changes. Not impossible to do but I think the current approach is a good starting point. We can certainly implement conversation forking in a subsequent PR, if needed. cc @slemeur

Fixes #1081

@fbricon fbricon requested a review from a team as a code owner March 19, 2026 10:13
@fbricon fbricon requested review from MarsKubeX and benoitf and removed request for a team March 19, 2026 10:13
@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

📝 Walkthrough

Walkthrough

Adds message-editing: IPC to remove trailing generated messages, a context-based EditState for editing lifecycle, UI changes to enter/cancel/edit messages (with grayed messages after the edited one), and tests + Playwright helpers covering edit/cancel and edit+regenerate flows.

Changes

Cohort / File(s) Summary
IPC / Preload
packages/main/src/chat/chat-manager.ts, packages/preload/src/index.ts
Registered new IPC channel inference:deleteTrailingMessages in main and exposed inferenceDeleteTrailingMessages(id) via contextBridge in preload.
EditState Hook
packages/renderer/src/lib/chat/hooks/edit-state.svelte.ts
New exported EditState class with context helpers, manages editingMessage, provides start/cancel editing and utilities to check if a message is after the edited message.
Chat Initialization
packages/renderer/src/lib/chat/components/chat.svelte
Instantiates EditState in chat root (calls EditState.toContext() after chat client derivation).
Message List & Preview
packages/renderer/src/lib/chat/components/messages.svelte, packages/renderer/src/lib/chat/components/messages/preview-message.svelte
Preview-message now receives full messages[], uses EditState.fromContext() to compute isGrayed for messages after edited message, removes local edit mode state and replaces edit interactions with EditState APIs; minor invocation change in messages loop.
Input / Multimodal
packages/renderer/src/lib/chat/components/multimodal-input.svelte
Integrates EditState to enter edit mode, snapshots/restores input, handles ESC to cancel, on submit deletes trailing messages via preload IPC, replaces edited message text and calls chatClient.regenerate() instead of sending a new message.
Tests & Page Object
tests/playwright/src/model/pages/chat-page.ts, tests/playwright/src/specs/provider-specs/chat-smoke.spec.ts
Added locators and helpers to operate edit UI (locate edit button, enter/cancel editing, assert grayed messages), plus two smoke tests: edit-cancel (CHAT-12) and edit+regenerate (CHAT-13).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as Chat UI
    participant EditState
    participant ChatClient
    participant IPC as Backend IPC

    User->>UI: Click "Edit message"
    UI->>EditState: startEditing(message)
    EditState-->>UI: editingMessage set, isEditing true
    UI->>UI: Populate textarea, gray messages after edited message

    alt Submit edited message
        User->>UI: Click send
        UI->>IPC: inference:deleteTrailingMessages(messageId)
        IPC-->>IPC: delete trailing messages in DB
        UI->>ChatClient: replace edited message text
        UI->>ChatClient: regenerate()
        ChatClient->>IPC: request regenerate
        UI->>EditState: cancelEditing()
    else Cancel editing
        User->>UI: Press ESC
        UI->>EditState: cancelEditing()
        UI->>UI: Restore textarea to saved input
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: implementing edit message functionality in the chat feature.
Description check ✅ Passed The description is well-detailed and directly related to the changeset, explaining the edit functionality implementation and approach.
Linked Issues check ✅ Passed The PR addresses issue #1081 by fixing the message disappearance bug and implementing the edit message functionality with proper UI state handling.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing edit message functionality: backend IPC handler, frontend UI state management, component updates, and test coverage.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte`:
- Around line 88-108: The block handling editState.editingMessage clears input
and cancels editing before calling await
window.inferenceDeleteTrailingMessages(editingMessage.id), which can fail and
leave the UI inconsistent; wrap the inferenceDeleteTrailingMessages call and the
subsequent update/regenerate logic in a try/catch, and on catch restore
editState.editingMessage (or call editState.startEditing with the original
editingMessage), restore the input via setInput(savedInput) and savedInput
variable, and surface the error (e.g., rethrow or log) so the user can recover
and the UI remains consistent; ensure chatClient.messages update and
chatClient.regenerate only run on success.

In `@tests/playwright/src/model/pages/chat-page.ts`:
- Around line 321-339: The helper verifyMessagesGrayedAfterIndex currently
matches the edited message by innerHTML which can incorrectly match duplicates;
change the lookup of editedGlobalIndex to compare DOM node identity between
editedMessage and items in allMessages (e.g., use a node identity check via
elementHandle.evaluate(...) with node.isSameNode or Playwright's node
comparison) instead of comparing innerHTML, and if no match is found throw a
clear error rather than leaving editedGlobalIndex as -1 so the trailing-opacity
assertions don't run from index 0; update references to allMessages,
userMessages, editedMessage, and editedGlobalIndex accordingly.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6d136b0c-14b3-42cd-b407-6d517d476956

📥 Commits

Reviewing files that changed from the base of the PR and between e7669bf and c48a709.

📒 Files selected for processing (9)
  • packages/main/src/chat/chat-manager.ts
  • packages/preload/src/index.ts
  • packages/renderer/src/lib/chat/components/chat.svelte
  • packages/renderer/src/lib/chat/components/messages.svelte
  • packages/renderer/src/lib/chat/components/messages/preview-message.svelte
  • packages/renderer/src/lib/chat/components/multimodal-input.svelte
  • packages/renderer/src/lib/chat/hooks/edit-state.svelte.ts
  • tests/playwright/src/model/pages/chat-page.ts
  • tests/playwright/src/specs/provider-specs/chat-smoke.spec.ts

Comment on lines +88 to +108
if (editState.editingMessage) {
const editingMessage = editState.editingMessage;
setInput('');
savedInput = '';
editState.cancelEditing();

await window.inferenceDeleteTrailingMessages(editingMessage.id);

const index = chatClient.messages.findIndex(m => m.id === editingMessage.id);
if (index !== -1) {
const updatedMessage = {
...editingMessage,
parts: [{ type: 'text' as const, text }],
};
chatClient.messages = [...chatClient.messages.slice(0, index), updatedMessage];
}

resetHeight();
await chatClient.regenerate();
return;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing error handling could leave the UI in an inconsistent state.

If inferenceDeleteTrailingMessages fails (e.g., database error, message already deleted), the editing state is already cancelled and the input cleared (lines 90-92), leaving the user with no recovery path. Consider wrapping this in try/catch to restore the editing state on failure.

🛡️ Proposed fix to add error handling
   if (editState.editingMessage) {
     const editingMessage = editState.editingMessage;
+    const originalInput = input;
     setInput('');
     savedInput = '';
     editState.cancelEditing();
 
-    await window.inferenceDeleteTrailingMessages(editingMessage.id);
+    try {
+      await window.inferenceDeleteTrailingMessages(editingMessage.id);
+    } catch (error) {
+      // Restore editing state on failure
+      editState.startEditing(editingMessage);
+      setInput(originalInput);
+      toast.error('Failed to update message. Please try again.');
+      return;
+    }
 
     const index = chatClient.messages.findIndex(m => m.id === editingMessage.id);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/renderer/src/lib/chat/components/multimodal-input.svelte` around
lines 88 - 108, The block handling editState.editingMessage clears input and
cancels editing before calling await
window.inferenceDeleteTrailingMessages(editingMessage.id), which can fail and
leave the UI inconsistent; wrap the inferenceDeleteTrailingMessages call and the
subsequent update/regenerate logic in a try/catch, and on catch restore
editState.editingMessage (or call editState.startEditing with the original
editingMessage), restore the input via setInput(savedInput) and savedInput
variable, and surface the error (e.g., rethrow or log) so the user can recover
and the UI remains consistent; ensure chatClient.messages update and
chatClient.regenerate only run on success.

Comment on lines +321 to +339
async verifyMessagesGrayedAfterIndex(userMessageIndex: number): Promise<void> {
const allMessages = await this.conversationMessages.all();
const userMessages = await this.userConversationMessages.all();
const editedMessage = userMessages[userMessageIndex];

// Find the position of the edited user message in all messages
let editedGlobalIndex = -1;
for (let i = 0; i < allMessages.length; i++) {
const el = allMessages[i];
if ((await el.innerHTML()) === (await editedMessage.innerHTML())) {
editedGlobalIndex = i;
break;
}
}

// Messages after the edited one should have reduced opacity
for (let i = editedGlobalIndex + 1; i < allMessages.length; i++) {
await expect(allMessages[i]).toHaveClass(/opacity-40/);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Match the edited message by DOM order, not innerHTML().

If two user messages render the same markup, Line 330 will match the first duplicate, so this helper can assert the wrong trailing messages. If nothing matches, editedGlobalIndex stays -1 and the loop starts from 0, which makes the failure misleading.

🛠️ Proposed fix
  async verifyMessagesGrayedAfterIndex(userMessageIndex: number): Promise<void> {
    const allMessages = await this.conversationMessages.all();
-    const userMessages = await this.userConversationMessages.all();
-    const editedMessage = userMessages[userMessageIndex];
-
-    // Find the position of the edited user message in all messages
-    let editedGlobalIndex = -1;
-    for (let i = 0; i < allMessages.length; i++) {
-      const el = allMessages[i];
-      if ((await el.innerHTML()) === (await editedMessage.innerHTML())) {
-        editedGlobalIndex = i;
-        break;
-      }
-    }
+    const editedGlobalIndex = await this.conversationMessages.evaluateAll((nodes, targetUserIndex) => {
+      let currentUserIndex = -1;
+
+      return nodes.findIndex(node => {
+        if ((node as HTMLElement).dataset.role !== 'user') {
+          return false;
+        }
+
+        currentUserIndex += 1;
+        return currentUserIndex === targetUserIndex;
+      });
+    }, userMessageIndex);
+    expect(editedGlobalIndex).toBeGreaterThanOrEqual(0);
 
     // Messages after the edited one should have reduced opacity
     for (let i = editedGlobalIndex + 1; i < allMessages.length; i++) {
       await expect(allMessages[i]).toHaveClass(/opacity-40/);
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async verifyMessagesGrayedAfterIndex(userMessageIndex: number): Promise<void> {
const allMessages = await this.conversationMessages.all();
const userMessages = await this.userConversationMessages.all();
const editedMessage = userMessages[userMessageIndex];
// Find the position of the edited user message in all messages
let editedGlobalIndex = -1;
for (let i = 0; i < allMessages.length; i++) {
const el = allMessages[i];
if ((await el.innerHTML()) === (await editedMessage.innerHTML())) {
editedGlobalIndex = i;
break;
}
}
// Messages after the edited one should have reduced opacity
for (let i = editedGlobalIndex + 1; i < allMessages.length; i++) {
await expect(allMessages[i]).toHaveClass(/opacity-40/);
}
async verifyMessagesGrayedAfterIndex(userMessageIndex: number): Promise<void> {
const allMessages = await this.conversationMessages.all();
const editedGlobalIndex = await this.conversationMessages.evaluateAll((nodes, targetUserIndex) => {
let currentUserIndex = -1;
return nodes.findIndex(node => {
if ((node as HTMLElement).dataset.role !== 'user') {
return false;
}
currentUserIndex += 1;
return currentUserIndex === targetUserIndex;
});
}, userMessageIndex);
expect(editedGlobalIndex).toBeGreaterThanOrEqual(0);
// Messages after the edited one should have reduced opacity
for (let i = editedGlobalIndex + 1; i < allMessages.length; i++) {
await expect(allMessages[i]).toHaveClass(/opacity-40/);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/playwright/src/model/pages/chat-page.ts` around lines 321 - 339, The
helper verifyMessagesGrayedAfterIndex currently matches the edited message by
innerHTML which can incorrectly match duplicates; change the lookup of
editedGlobalIndex to compare DOM node identity between editedMessage and items
in allMessages (e.g., use a node identity check via elementHandle.evaluate(...)
with node.isSameNode or Playwright's node comparison) instead of comparing
innerHTML, and if no match is found throw a clear error rather than leaving
editedGlobalIndex as -1 so the trailing-opacity assertions don't run from index
0; update references to allMessages, userMessages, editedMessage, and
editedGlobalIndex accordingly.

Copy link
Contributor

@MarsKubeX MarsKubeX left a comment

Choose a reason for hiding this comment

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

LGTM.

@benoitf
Copy link
Contributor

benoitf commented Mar 19, 2026

I've restarted the failing job

Clicking the edit button on a user message populates the input box with
the message text, shows a "Press ESC to cancel editing" hint, and grays
out all messages after the edited one. Submitting the edit deletes
trailing messages from the database and regenerates the AI response.

- Add EditState context for shared editing state across components
- Add deleteTrailingMessages IPC endpoint and preload exposure
- Update multimodal-input to handle edit mode (ESC cancel, submit edit)
- Update preview-message to gray out messages and hide edit buttons
  during editing
- Add e2e tests for edit cancel (CHAT-12) and edit submit (CHAT-13)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Signed-off-by: Fred Bricon <fbricon@gmail.com>
@benoitf benoitf force-pushed the edit-button-implem branch from c48a709 to a2c6c4e Compare March 19, 2026 17:23
@benoitf
Copy link
Contributor

benoitf commented Mar 19, 2026

clicked on the rebase button as the other pr was merged ( commit included in this pr)

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
tests/playwright/src/model/pages/chat-page.ts (1)

321-339: ⚠️ Potential issue | 🟡 Minor

Use deterministic message indexing instead of innerHTML() matching.

At Line 330, matching by innerHTML() can misidentify the edited message when content repeats. If no match is found, editedGlobalIndex remains -1, and Line 337 starts assertions from index 0, producing misleading failures.

🛠️ Suggested fix
   async verifyMessagesGrayedAfterIndex(userMessageIndex: number): Promise<void> {
     const allMessages = await this.conversationMessages.all();
-    const userMessages = await this.userConversationMessages.all();
-    const editedMessage = userMessages[userMessageIndex];
-
-    // Find the position of the edited user message in all messages
-    let editedGlobalIndex = -1;
-    for (let i = 0; i < allMessages.length; i++) {
-      const el = allMessages[i];
-      if ((await el.innerHTML()) === (await editedMessage.innerHTML())) {
-        editedGlobalIndex = i;
-        break;
-      }
-    }
+    const editedGlobalIndex = await this.conversationMessages.evaluateAll((nodes, targetUserIndex) => {
+      let currentUserIndex = -1;
+      return nodes.findIndex(node => {
+        if ((node as HTMLElement).dataset.role !== 'user') return false;
+        currentUserIndex += 1;
+        return currentUserIndex === targetUserIndex;
+      });
+    }, userMessageIndex);
+    expect(editedGlobalIndex).toBeGreaterThanOrEqual(0);
 
     // Messages after the edited one should have reduced opacity
     for (let i = editedGlobalIndex + 1; i < allMessages.length; i++) {
       await expect(allMessages[i]).toHaveClass(/opacity-40/);
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/playwright/src/model/pages/chat-page.ts` around lines 321 - 339, The
test uses innerHTML to match elements which can misidentify duplicates and leave
editedGlobalIndex at -1; modify verifyMessagesGrayedAfterIndex to locate the
edited message deterministically by comparing node identity between arrays (use
ElementHandle.evaluate with node.isSameNode or similar) to set
editedGlobalIndex, add a guard that throws/fails if editedGlobalIndex remains
-1, then proceed with the opacity assertions on conversationMessages; reference
function verifyMessagesGrayedAfterIndex, variables conversationMessages,
userConversationMessages, and editedGlobalIndex.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/playwright/src/model/pages/chat-page.ts`:
- Around line 321-339: The test uses innerHTML to match elements which can
misidentify duplicates and leave editedGlobalIndex at -1; modify
verifyMessagesGrayedAfterIndex to locate the edited message deterministically by
comparing node identity between arrays (use ElementHandle.evaluate with
node.isSameNode or similar) to set editedGlobalIndex, add a guard that
throws/fails if editedGlobalIndex remains -1, then proceed with the opacity
assertions on conversationMessages; reference function
verifyMessagesGrayedAfterIndex, variables conversationMessages,
userConversationMessages, and editedGlobalIndex.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08a57fe6-fc71-4c44-8a00-21a7b1f272ff

📥 Commits

Reviewing files that changed from the base of the PR and between c48a709 and a2c6c4e.

📒 Files selected for processing (9)
  • packages/main/src/chat/chat-manager.ts
  • packages/preload/src/index.ts
  • packages/renderer/src/lib/chat/components/chat.svelte
  • packages/renderer/src/lib/chat/components/messages.svelte
  • packages/renderer/src/lib/chat/components/messages/preview-message.svelte
  • packages/renderer/src/lib/chat/components/multimodal-input.svelte
  • packages/renderer/src/lib/chat/hooks/edit-state.svelte.ts
  • tests/playwright/src/model/pages/chat-page.ts
  • tests/playwright/src/specs/provider-specs/chat-smoke.spec.ts
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/renderer/src/lib/chat/components/chat.svelte
  • tests/playwright/src/specs/provider-specs/chat-smoke.spec.ts
  • packages/renderer/src/lib/chat/hooks/edit-state.svelte.ts
  • packages/renderer/src/lib/chat/components/multimodal-input.svelte
  • packages/main/src/chat/chat-manager.ts

@benoitf benoitf enabled auto-merge (rebase) March 19, 2026 17:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Message disappears when clicking "Edit message"

3 participants