Skip to content

Allow cancelling chat message generation - #696

Open
WSHAPER wants to merge 8 commits into
nextcloud:mainfrom
WSHAPER:feat/cancel-generation
Open

WSHAPER wants to merge 8 commits into
nextcloud:mainfrom
WSHAPER:feat/cancel-generation

Conversation

@WSHAPER

@WSHAPER WSHAPER commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

While a reply is being generated, the send button turns into a stop button and Escape cancels as well. The underlying Task Processing task is cancelled through the core cancel endpoint, and the generation poll treats a cancelled task as an expected outcome, so the session is usable again right away.

Escape is consumed while a generation is running, so the key press does not leak into the host app. Otherwise, with the assistant opened over Talk, pressing Escape would also deselect the Talk conversation.

Fixes #678

Recon (fully) and implementation (partially) assisted-by: opencode:glm-5.3

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

The input area displays a stop button during generation and emits a cancellation event when clicked. The form also supports cancellation with Escape. It tracks task IDs during session checks, generation, and regeneration. Polling treats the cancelled-task response as an expected cancellation.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: 🟡 Moderate · up to f5713

Session switches or overlapping session checks can leave chat generation stuck or appear idle while work continues. Resolve these race conditions before merging; idle Escape handling is a separate, lower-impact issue.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Out of Scope Changes check Warning onKeydownCancelGeneration stops propagation for every Escape key inside the component when assignment and deletion modes are inactive. It does this even when loading.llmGeneration is false. This c… Only stop propagation and call cancelGeneration when loading.llmGeneration is true. Let Escape propagate when no generation is active.
Passed Passed checks: 4 passed
Check name Status Explanation
Description check Passed The description clearly explains the stop button, Escape-key cancellation, Task Processing cancellation, polling behavior, and host-app handling. It directly matches the changeset.
Title check Passed The title clearly and concisely summarizes the main change: allowing users to cancel chat message generation.
Linked Issues check Passed The changes meet the coding requirements in #678. InputArea.vue shows a stop button whenever loading.llmGeneration is true, including empty-input and audio-chat states. ChattyLLMInputForm.vue em…
Docstring Coverage Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
Full details: Out of Scope Changes check

Explanation

onKeydownCancelGeneration stops propagation for every Escape key inside the component when assignment and deletion modes are inactive. It does this even when loading.llmGeneration is false. This changes host-app Escape behavior outside the generation-cancellation scope of #678.

  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4f5b6e17-46bd-4010-8f73-45da94313915
📥 Commits

Reviewing files that changed from the base of the PR and between d5a4cf0 and 9e26cfb.

📒 Files selected for processing (2)
  • src/components/ChattyLLM/ChattyLLMInputForm.vue
  • src/components/ChattyLLM/InputArea.vue

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue Outdated
Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue
Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue Outdated
Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue
Fixes nextcloud#678

Assisted-by: opencode:glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
@WSHAPER
WSHAPER force-pushed the feat/cancel-generation branch from 9e26cfb to 62dd6c4 Compare October 13, 2026 09:51
@WSHAPER

WSHAPER commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

All four comments addressed:

  • the stop request is deferred and executed once the task id is known
  • the check_session cleanup and the task tracking are guarded by task ownership and the active session, so a generation scheduled while a check is running keeps its state, and the stop control can never cancel the wrong session's task
  • the streamed partial output is cleared in the finally block, also when the poll ends on a cancelled task

Verified manually in a headed browser: stop click during scheduling, cancel of a resumed (reloaded) generation, and Escape not leaking into the host app's own handlers.

@WSHAPER
WSHAPER requested a review from julien-nc October 13, 2026 10:56

@julien-nc julien-nc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice!

  • When submitting a message, the right icon briefly switches to the audio one and then becomes the stop one. Could this be avoided?
  • There's a delay of 1 or 2 seconds between the click on "stop" and the UI reaction. Could it be faster? I see the UI reacts after the next check_generation is done. We could probably consider that if the cancel request is successful, no need to continue polling. We can stop polling and change the form state immediately. Wdyt?

Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
@WSHAPER

WSHAPER commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Thanks!

Both addressed now:

  • When submitting a message, the right icon briefly switches to the audio one and then becomes the stop one. Could this be avoided?

The flicker: the send button now stays visible, disabled, through the whole submit round-trip and the microphone only renders while the input is actually usable. No more detour through the audio icon.

  • There's a delay of 1 or 2 seconds between the click on "stop" and the UI reaction. Could it be faster? I see the UI reacts after the next check_generation is done. We could probably consider that if the cancel request is successful, no need to continue polling. We can stop polling and change the form state immediately. Wdyt?

The delay: went with exactly that. When the cancel request succeeds we stop the generation poll and reset the form state right away instead of waiting for the next check_generation tick. The reaction time is now just the cancel round-trip, around 200ms locally where it could hit 2s+ before.

@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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e11724b5-d799-4860-b1b2-18ed8a9d9c20
📥 Commits

Reviewing files that changed from the base of the PR and between 9e26cfb and b6eba7f.

📒 Files selected for processing (2)
  • src/components/ChattyLLM/ChattyLLMInputForm.vue
  • src/components/ChattyLLM/InputArea.vue

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue
Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue
Assisted-by: opencode:glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
@WSHAPER

WSHAPER commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

The pending stop is now owned by its scheduling request via a sequence number, so a failed scheduling round clears its own stop request and can never trigger or clear another generation's. And the poll handle is captured before the cancel request is sent, so a session switch mid-request can no longer finalize the next generation's poll.

@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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c62b0738-517a-4147-b67b-7e909721f090
📥 Commits

Reviewing files that changed from the base of the PR and between b6eba7f and 5151ec9.

📒 Files selected for processing: 1
  • src/components/ChattyLLM/ChattyLLMInputForm.vue

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue
Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue
Assisted-by: opencode:glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
@WSHAPER

WSHAPER commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

@julien-nc CodeRabbit reviews currently rate limited, but everything's been addressed so far.
If you have any further suggestions, I'm looking forward to it.

@julien-nc julien-nc left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Edge case: When cancelling a generation that was triggered by clicking "Regenerate message" on a message, the message that was supposed to be regenerated stays visible in the UI BUT it has actually been deleted on the backend side already.

Image

Switching to another conversation and back to the one (or reloading the page) makes the message disappear (for good reasons, it does not exist anymore).
The same happens for a failed "regeneration" (so there was a bug even before introducing cancellation).

2 ways forward IMO:

  1. Just make sure the message is removed/deleted in the frontend because we know it is deleted in the backend. Problem: in case of failure/cancellation, the message is gone and one must send another message to continue the conversation.
  2. Implement a proper fix where the old message is deleted only when the regeneration is successful. This fix does not belong in this PR. It could be a separate issue and a later PR.

I would say: fine with both 1. first and 2. later (let's still create the issue now).

Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue Outdated
Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue
Comment thread src/components/ChattyLLM/ChattyLLMInputForm.vue Outdated
- only finalize the cancelling task's own poll, a session switch during
  the cancel request must not clobber the next generation's poll handle
- drop the regenerated message from the UI when a regeneration is
  cancelled or fails, the backend already deleted it at scheduling time

Assisted-by: opencode:glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟠 Major · Do not start polling after task ownership is lost. · ChattyLLMInputForm.vue:1204

src/components/ChattyLLM/ChattyLLMInputForm.vue:1204
🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not start polling after task ownership is lost.

trackGenerationTask returns when the session is inactive, but each caller still starts task-position and generation polling. The session watcher cancels only the poll that exists at the time of the switch. A late scheduling response can install a stale poll and replace the active session’s shared poll handle. The active poll then ignores its responses, so its completion promise can remain pending. The stale task can also reset the active generation state in either finalizer.

Check session ownership before starting either poll, and guard generation cleanup with both the session ID and request sequence.

Suggested fix
@@ checkSession(sessionId, isAssignment)
 				if (checkSessionResponseData.messageTaskId !== null) {
 					try {
 						this.loading.llmGeneration = true
 						this.loading.llmRunning = false
 						checkTaskId = checkSessionResponseData.messageTaskId
+						if (this.active?.id !== sessionId) {
+							return
+						}
 						this.trackGenerationTask(checkTaskId, sessionId, this.generationRequestSeq)
 						this.startTaskPositionPolling(checkSessionResponseData.messageTaskId, sessionId)
@@ runGenerationTask(sessionId, agencyConfirm = null)
 				console.debug('scheduleGenerationTask response:', generationResponseData)
 				taskId = generationResponseData.taskId
 				this.trackGenerationTask(taskId, sessionId, requestSeq)
+				if (this.active?.id !== sessionId || requestSeq !== this.generationRequestSeq) {
+					return
+				}
 				this.startTaskPositionPolling(taskId, sessionId)
 				const message = await this.pollGenerationTask(taskId, sessionId)
@@
 			} finally {
+				if (this.active?.id !== sessionId || requestSeq !== this.generationRequestSeq) {
+					return
+				}
 				if (this.pollPositionSessionId === sessionId) {

Apply the same ownership check before polling and at the start of the finally block in runRegenerationTask.


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a0c5c163-0df9-4f83-b3a1-4cd415d145d8
📥 Commits

Reviewing files that changed from the base of the PR and between 5151ec9 and f65eceb.

📒 Files selected for processing: 1
  • src/components/ChattyLLM/ChattyLLMInputForm.vue

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

@WSHAPER

WSHAPER commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough feedback, @julien-nc.
I hope you don't mind the squashed commit.

When cancelling a generation that was triggered by clicking "Regenerate message" on a message, the message that was supposed to be regenerated stays visible in the UI BUT it has actually been deleted on the backend side already.

Went with 1. in f65eceb: a cancelled or failed regeneration now removes the message in the frontend.

  1. Implement a proper fix where the old message is deleted only when the regeneration is successful. This fix does not belong in this PR. It could be a separate issue and a later PR.

…stop control

Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>

@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


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 95c4fc96-cef2-47b3-a14c-b24c581595db
📥 Commits

Reviewing files that changed from the base of the PR and between f65eceb and f5713b8.

📒 Files selected for processing: 2
  • src/components/ChattyLLM/ChattyLLMInputForm.vue
  • src/components/ChattyLLM/InputArea.vue

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

-->
<template>
<div class="container">
<div class="container" @keydown="onKeydownCancelGeneration">

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 | 🟡 Minor | ⚡ Quick win

Let Escape reach the host when generation is idle.

When focus is inside this container, onKeydownCancelGeneration stops Escape propagation even if loading.llmGeneration is false. cancelGeneration then returns without action. This prevents host-app Escape handlers from working during normal chat use. Check loading.llmGeneration before consuming the key.

Assisted-by: opencode:glm-5.3
Signed-off-by: WSHAPER <42714629+WSHAPER@users.noreply.github.com>
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.

Allow cancelling chat message generation

2 participants