Skip to content

feat(web): add message edit, retry, and copy actions#48

Merged
cnjack merged 2 commits intomainfrom
feat/web-chat-message-actions
Apr 26, 2026
Merged

feat(web): add message edit, retry, and copy actions#48
cnjack merged 2 commits intomainfrom
feat/web-chat-message-actions

Conversation

@cnjack
Copy link
Copy Markdown
Owner

@cnjack cnjack commented Apr 26, 2026

Summary

  • add copy actions to all chat messages in the web UI
  • add retry for assistant messages and inline edit for user messages
  • truncate backend history and rewrite the session file in place so retries/edits keep the same session UUID
  • fix locking and zero-count truncation edge cases in session history handling

Verification

  • go build ./...
  • TypeScript errors checked in edited frontend files

Summary by CodeRabbit

Release Notes

  • New Features
    • Edit messages: Modify and resend your messages inline while the chat is idle
    • Retry responses: Regenerate assistant responses with a single click
    • Copy to clipboard: Easily copy message content to your clipboard
    • Smart availability: Edit and retry options appear only when chat is not running

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Warning

Rate limit exceeded

@cnjack has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 13 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 49 minutes and 13 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ad6eb30-66c9-408b-ba04-bb02b9b3f8fc

📥 Commits

Reviewing files that changed from the base of the PR and between 07c0fea and c11845a.

📒 Files selected for processing (3)
  • internal/web/server.go
  • web/src/components/ChatMessage.vue
  • web/src/stores/chat.ts
📝 Walkthrough

Walkthrough

The changes introduce history truncation functionality for chat conversations. A new backend method truncates session files at specified user message boundaries, a new HTTP endpoint handles history truncation requests, and the frontend adds message editing and retry capabilities with store actions and UI components to support conversation correction.

Changes

Cohort / File(s) Summary
Backend Session Truncation
internal/session/session.go
Adds TruncateAtUserMessage method that reads JSONL session file line-by-line, retains session_start entry, truncates at the specified user message boundary, atomically rewrites the file, and reopens for append.
Backend HTTP Endpoint
internal/web/server.go
Introduces /api/history/truncate endpoint that reads before_user_message from request, truncates in-memory history slice at the corresponding user message index, calls TruncateAtUserMessage on the active recorder, and returns session ID.
Frontend API Client
web/src/composables/api.ts
Adds truncateHistory method that POSTs to /api/history/truncate with before_user_message parameter and returns backend response with status and session_id.
Frontend Store Actions
web/src/stores/chat.ts
Introduces retryFromMessage and editAndResend actions that count preceding user messages, truncate backend history, clear streaming state, and resend user content (original or edited).
Frontend Message UI
web/src/App.vue, web/src/components/ChatMessage.vue
Adds message interaction UI with conditional can-retry and can-edit props, event handlers for retry and edit events, inline textarea edit mode with keyboard handling, copy-to-clipboard functionality, and Send/Cancel buttons.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as ChatMessage.vue
    participant Store as useChatStore
    participant API as api.ts
    participant Server as server.go
    participant Session as session.go
    
    User->>UI: Click edit/retry on message
    UI->>Store: Emit retry or edit event
    Store->>Store: Count user messages before target
    Store->>API: truncateHistory(beforeCount)
    API->>Server: POST /api/history/truncate
    Server->>Server: Truncate in-memory history slice
    Server->>Session: TruncateAtUserMessage(beforeCount)
    Session->>Session: Read JSONL, keep up to boundary
    Session->>Session: Atomic file rewrite & rename
    Session-->>Server: Success
    Server-->>API: {status, session_id}
    API-->>Store: Response received
    Store->>Store: Update currentSessionId
    Store->>Store: Clear streaming state
    Store->>Store: Splice/update timeline
    Store->>API: Send user message
    UI->>UI: Update displayed history
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 With whiskers twitching, I edit my plea,
Truncate the past, let new words flow free,
A retry button, a chance to resend,
History shortened to where we'll blend! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 'feat(web): add message edit, retry, and copy actions' directly and concisely describes the main changes: it introduces three new user-facing message actions (edit, retry, copy) in the web UI, which is the primary focus of the changeset across web components and store logic.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-chat-message-actions

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
Copy Markdown

@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: 6

🧹 Nitpick comments (1)
web/src/stores/chat.ts (1)

599-626: Validate the target is a user message; consider extracting shared logic.

editAndResend doesn't verify the looked‑up message has role === 'user' — if it's ever invoked on an assistant message (e.g., the canEdit gating is bypassed via component reuse), the count becomes wrong (counts users before an assistant index) and the wrong portion of the conversation is truncated. Adding a guard makes the action robust independent of UI gating.

Also, retryFromMessage and editAndResend share the same truncation+resend pipeline (count → api.truncateHistory → splice → reset streaming → sendMessage). Extracting a private helper would remove the duplication and keep the failure handling consistent in one place.

♻️ Proposed sketch
 async function editAndResend(messageId: string, newText: string) {
-    // Find the user message to edit
-    const msgIdx = timeline.value.findIndex(i => i.kind === 'message' && i.data.id === messageId)
-    if (msgIdx === -1) return
+    // Find the user message to edit
+    const msgIdx = timeline.value.findIndex(
+      i => i.kind === 'message' && i.data.id === messageId && i.data.role === 'user',
+    )
+    if (msgIdx === -1) return
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/stores/chat.ts` around lines 599 - 626, The editAndResend function
can operate on non-user messages and miscount truncation; add a guard after
locating msgIdx to verify timeline.value[msgIdx].data.role === 'user' (return
early if not) to ensure the count of beforeUserMessage is correct; then extract
the shared truncation+resend pipeline used by editAndResend and retryFromMessage
into a private helper (e.g., truncateAndResend(beforeUserCount, newText)) that
calls api.truncateHistory, updates currentSessionId, splices timeline.value from
the given index, resets streamingText and streamingMsgId, and finally calls
sendMessage, consolidating error handling and keeping behavior consistent across
both callers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/web/server.go`:
- Around line 627-639: handleTruncateHistory currently mutates s.history and
rewrites the session file without checking s.running, causing races with the
agent goroutine; update the handler (function handleTruncateHistory) to check
s.running.Load() at the start and if running return HTTP 409 (like
handleSwitchModel/handleSwitchProject) before acquiring s.mu, thus rejecting
truncation while a run is in flight and preventing concurrent edits to s.history
and session file.
- Around line 674-686: The in-memory history (s.history) is being truncated
before persisting and when rec.TruncateAtUserMessage fails the client still
receives 200 OK and state diverges; fix by: perform the on-disk truncation with
rec.TruncateAtUserMessage before mutating s.history (or if you must mutate
first, revert s.history on failure), on error return an HTTP error response (use
writeJSON with 500 and the error message) instead of writing 200, and
mark/disable the recorder (e.g., set the recorder reference or a recorderActive
flag on the session) to prevent further appends until a successful truncation is
confirmed; locate the logic around rec.TruncateAtUserMessage, s.history
mutation, and the writeJSON response to implement these changes.

In `@web/src/components/ChatMessage.vue`:
- Around line 89-131: The action buttons row in ChatMessage.vue is hidden via
opacity (class "opacity-0 group-hover/msg:opacity-100") which makes keyboard
users tab to invisible controls; update the visibility rules so the buttons are
also revealed on keyboard focus — either add a focus-within variant to the
parent group (e.g., "group-focus-within/msg:opacity-100") or add a focus-visible
opacity override on each button (e.g., "focus-visible:opacity-100" on the
buttons that call copyContent, emit('retry'), and startEdit) so canRetry/canEdit
buttons become visible when focused; ensure you adjust the same element that
currently uses "group-hover/msg:opacity-100" so it covers the copyContent,
retry, and startEdit controls.
- Around line 21-26: The copyContent function calls
navigator.clipboard.writeText(props.message.content) without handling promise
rejections; update copyContent to attach a .catch handler to
navigator.clipboard.writeText so failures are logged (e.g., console.error) and
the UI still gives feedback (for example set copied.value to false or set a
transient error state) and ensure the timeout cleanup still runs; reference the
copyContent function, copied reactive ref, and props.message.content when
implementing the change.
- Around line 149-155: The textarea’s HTML autofocus is unreliable when it’s
mounted dynamically; replace the autofocus attribute with a template ref (e.g.,
editTextareaRef) on the textarea and, in the ChatMessage component, use Vue's
nextTick to call focus() (and optionally select()) when editing.value becomes
true — implement this either in the method that sets editing.value = true or in
a watch on editing to run nextTick(() => editTextareaRef.value?.focus() ||
editTextareaRef.value?.select()); remove the static autofocus attribute and
ensure the ref name matches where handleEditKeyDown and editText are used so the
textarea receives keyboard focus immediately after mount.

In `@web/src/stores/chat.ts`:
- Around line 580-597: The try/catch around api.truncateHistory should stop the
flow on error instead of proceeding to mutate the frontend timeline and call
sendMessage; update the logic in the function containing this block so that if
api.truncateHistory throws you log/surface the error to the user (e.g., via the
existing UI error handler or a visible notification), do not call
timeline.value.splice(userMsgIdx), do not reset streamingText/streamingMsgId,
and return early (preventing sendMessage from being invoked); reference
api.truncateHistory, currentSessionId, timeline.value.splice, and sendMessage to
locate and modify the code accordingly.

---

Nitpick comments:
In `@web/src/stores/chat.ts`:
- Around line 599-626: The editAndResend function can operate on non-user
messages and miscount truncation; add a guard after locating msgIdx to verify
timeline.value[msgIdx].data.role === 'user' (return early if not) to ensure the
count of beforeUserMessage is correct; then extract the shared truncation+resend
pipeline used by editAndResend and retryFromMessage into a private helper (e.g.,
truncateAndResend(beforeUserCount, newText)) that calls api.truncateHistory,
updates currentSessionId, splices timeline.value from the given index, resets
streamingText and streamingMsgId, and finally calls sendMessage, consolidating
error handling and keeping behavior consistent across both callers.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 80ac09c7-e6af-4ed4-97ca-e8c63a83560e

📥 Commits

Reviewing files that changed from the base of the PR and between 2d206f3 and 07c0fea.

📒 Files selected for processing (6)
  • internal/session/session.go
  • internal/web/server.go
  • web/src/App.vue
  • web/src/components/ChatMessage.vue
  • web/src/composables/api.ts
  • web/src/stores/chat.ts

Comment thread internal/web/server.go
Comment thread internal/web/server.go Outdated
Comment thread web/src/components/ChatMessage.vue
Comment thread web/src/components/ChatMessage.vue Outdated
Comment thread web/src/components/ChatMessage.vue
Comment thread web/src/stores/chat.ts
- Guard truncate endpoint with s.running check (409 if running)
- Persist session file before mutating in-memory history; return 500 on failure
- Add keyboard accessibility (focus-within) for action buttons
- Handle clipboard writeText rejection
- Replace HTML autofocus with template ref + nextTick
- Return early in retry/edit if backend truncation fails
- Add role === 'user' guard in editAndResend
@cnjack cnjack merged commit 9b3d704 into main Apr 26, 2026
1 check passed
@cnjack cnjack deleted the feat/web-chat-message-actions branch April 26, 2026 16:16
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.

1 participant