Skip to content

feat(agency): Show item count and reversibility in confirmation dialog - #732

Merged
marcelklehr merged 5 commits into
mainfrom
feat/agency-confirmation-reversibility-itemcount
Nov 10, 2026
Merged

marcelklehr merged 5 commits into
mainfrom
feat/agency-confirmation-reversibility-itemcount

Conversation

@marcelklehr

@marcelklehr marcelklehr commented Nov 9, 2026 •

Copy link
Copy Markdown
Member

2026-11-09 - Marcel Klehr- Assistant agency confirmation with destructive batch action.webm

2026-11-09 - Marcel Klehr - Assistant agency confirmation with irreversible action.webm

🤖 AI (if applicable)

  • The content of this PR was partly or fully generated using AI (N/A)

… backends

Signed-off-by: Marcel Klehr <mklehr@gmx.net>
…to their own icons

Signed-off-by: Marcel Klehr <mklehr@gmx.net>
@marcelklehr
marcelklehr requested a review from julien-nc November 9, 2026 08:29
@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Explain Change →

Walkthrough

AgencyConfirmation now warns when an action is destructive or irreversible. Its footer displays separate conditional popovers for radius, destructive status, irreversibility, and affected-item count. The component sums finite item counts and marks the total as exact only when every action has an exact finite count. Radius calculation ignores actions with no reported radius, maps unknown reported values to external, and returns no radius when none is reported.

Merge Risk: Low · up to 24301

Some confirmations can omit a specific content-loss warning or hide an inexact one-item count. The general sensitive-action notice remains, so this is a bounded risk to address before merge or accept explicitly.

Pre-merge checks: Passed 5

Check name Status Explanation
Title check Passed The title clearly describes the confirmation dialog changes, including item count and reversibility.
Description check Passed The demo videos show destructive and irreversible actions in the agency confirmation dialog, which relates to the changeset.
Docstring Coverage Passed No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
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.
Full details: Docstring Coverage

Explanation

No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 0 functions across 0 files. (1 skipped: 1 unsupported.)

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Warning

Some tools did not complete. Review the errors below.

ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/components/ChattyLLM/AgencyConfirmation.vue

typescript-eslint does not support TS 7.0.
Please see https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0 to run typescript-eslint using the TS 6 API.
See also typescript-eslint/typescript-eslint#10940 for tracking typescript-eslint's support for TS >=7.1

Oops! Something went wrong! :(

ESLint: 10.14.0

Error: typescript-eslint does not support TS 7.0.
at Object. (/.eslint-tmp/node_modules/typescript-eslint/dist/index.js:52:11)
at Module._compile (node:internal/modules/cjs/loader:1929:14)
at Object..js (node:internal/modules/cjs/loader:2060:10)
at Module.load (node:internal/modules/cjs/loader:1651:32)
at Module._load (node:internal/modules/cjs/loader:1443:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)
at loadCJSModuleWithModuleLoad (node:internal/modules/esm/translators:372:15)
at ModuleWrap. (node:internal/modules/esm/translators:244:9)
at ModuleJob.run (node:internal/modules/esm/module_job:561:25)
at async node:internal/modules/esm/loader:647:26

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

@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

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 8b21bab9-a6b0-486e-beb0-bfacafa2cbe0
Commits

Reviewing files that changed from the base of the PR and between 9e9a064 and 534adfb.

Files selected for processing: 1

  • src/components/ChattyLLM/AgencyConfirmation.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 on lines +168 to +170
// actions that don't report a destructive state (older backends) are assumed to be non-destructive
for (const action of this.actions) {
if (action?.destructive !== false) {
if (action?.destructive !== undefined && action?.destructive !== null && action.destructive !== false) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Minor · Functional Correctness · Quick win

Supported by static analysis

Script executed:

grep -rn "destructive\|reversibility\|item_count\|impulse_radius" src lib --include=*.vue --include=*.js --include=*.php | head -50

Repository: nextcloud/assistant

Length of output: 2057


Script executed:

git diff 9e9a064a37de8ef5a55711e821a7ce0b11f34ea7 534adfbb0fbc50b966d035507f03ac5e423d6b39 -- src/components/ChattyLLM/AgencyConfirmation.vue
nl -ba src/components/ChattyLLM/AgencyConfirmation.vue | sed -n '1,280p'
rg -n -F -e 'sessionAgencyPendingActions' -e 'AgencyConfirmation' -e 'destructive' -e 'context_agent' -- src lib appinfo tests package.json composer.json || test "$?" -eq 1

Repository: nextcloud/assistant

Length of output: 19496


Script executed:

rg -n -F -e 'improveAgencyActionNames' -e 'getAgencyPendingActions' -e 'setAgencyPendingActions' -e 'agencyPendingActions' -e 'agencyAnswered' -e 'confirmAgency' -- lib src tests || test "$?" -eq 1
nl -ba lib/Controller/ChattyLLMController.php | sed -n '570,735p'
nl -ba src/components/ChattyLLM/AgencyActions.vue | sed -n '1,240p'
nl -ba src/components/ChattyLLM/ChattyLLMInputForm.vue | sed -n '180,225p;610,645p;825,890p;1470,1510p'
rg -n -F -e 'context_agent' -e 'nextcloud' -- appinfo/info.xml composer.json README.md | sed -n '1,160p'

Repository: nextcloud/assistant

Length of output: 24945


Web query:

site:github.com/nextcloud/context_agent "destructive" "impulse_radius" pending actions older backend

Result:

Sources consulted:
- GitHub - nextcloud/context_agent · GitHub — https://github.com/nextcloud/context_agent
- Releases - Nextcloud Context Agent - Apps - App Store - Nextcloud — https://apps.nextcloud.com/apps/context_agent/releases?platform=31
- context_agent/CHANGELOG.md at main · nextcloud/context_agent · GitHub — https://github.com/nextcloud/context_agent/blob/main/CHANGELOG.md
- Issues · nextcloud/context_agent · GitHub — https://github.com/nextcloud/context_agent/issues
- …and 29 more

Script executed:

nl -ba lib/Controller/ChattyLLMController.php | sed -n '155,215p'
nl -ba lib/Listener/ChattyLLMTaskListener.php | sed -n '110,185p'
nl -ba src/components/ChattyLLM/AgencyAction.vue | sed -n '1,220p'
nl -ba src/components/ChattyLLM/ChattyLLMInputForm.vue | sed -n '1620,1685p'
python3 - <<'PY'
import json, urllib.request
url = 'https://api.github.com/repos/nextcloud/context_agent/git/trees/2.8.0?recursive=1'
try:
    with urllib.request.urlopen(urllib.request.Request(url, headers={'User-Agent':'evidence-inspection'}), timeout=20) as r:
        data = json.load(r)
    print('TREE SHA:',data.get('sha'))
    for entry in data.get('tree', []):
        p=entry['path']
        if p.endswith('.py') and any(w in p.lower() for w in ['tool','agent','task','confirmation']):
            print(p)
except Exception as e:
    print('FETCH ERROR:',type(e).__name__,str(e))
PY

Repository: nextcloud/assistant

Length of output: 12445


Web query:

github nextcloud context_agent "pending_actions" "delete" 2.8.0 tool confirmation

Result:

Sources consulted:
- context_agent/CHANGELOG.md at main · nextcloud/context_agent · GitHub — https://github.com/nextcloud/context_agent/blob/main/CHANGELOG.md
- Releases - Nextcloud Context Agent - Apps - App Store - Nextcloud — https://apps.nextcloud.com/apps/context_agent/releases?platform=31
- Releases - Nextcloud Context Agent - Apps - App Store - Nextcloud — https://apps.nextcloud.com/apps/context_agent/releases?platform=34
- …and 45 more

Script executed:

python3 - <<'PY'
import json, urllib.request
base='https://api.github.com/repos/nextcloud/context_agent/'
def get(path):
    with urllib.request.urlopen(urllib.request.Request(base+path, headers={'User-Agent':'evidence-inspection'}), timeout=30) as r: return json.load(r)
try:
    tags=get('tags?per_page=100')
    print('Relevant tags:', [(t['name'],t['commit']['sha']) for t in tags if any(v in t['name'] for v in ('2.8','2.9','2.10'))])
    for t in tags:
        if '2.8.0' in t['name']:
            data=get('git/trees/'+t['commit']['sha']+'?recursive=1')
            print('TREE',t['name'],data.get('sha'),'truncated',data.get('truncated'))
            for entry in data.get('tree',[]):
                p=entry['path']
                if p.endswith('.py') and any(w in p.lower() for w in ['tool','agent','task','confirmation']): print(p)
            break
except Exception as e:
    print('FETCH ERROR:',type(e).__name__,str(e))
PY

Repository: nextcloud/assistant

Length of output: 1636


Script executed:

python3 - <<'PY'
import urllib.request
root='https://raw.githubusercontent.com/nextcloud/context_agent/99936dd8c518b164aae8bdb12e965a640fe6f1fc/'
paths=['ex_app/lib/agent.py','ex_app/lib/all_tools/files.py','ex_app/lib/all_tools/forms.py','ex_app/lib/all_tools/lib/decorator.py','ex_app/lib/tools.py']
for p in paths:
    try:
        with urllib.request.urlopen(root+p, timeout=25) as r: lines=r.read().decode().splitlines()
        print('\n###',root+p)
        hits=[i for i,l in enumerate(lines) if any(w in l.lower() for w in ['pending','interrupt','confirm','delete_','destructive','reversibility','actions'])]
        spans=[]
        for i in hits:
            a=max(0,i-5); b=min(len(lines),i+11)
            if spans and a<=spans[-1][1]: spans[-1]=(spans[-1][0],max(b,spans[-1][1]))
            else: spans.append((a,b))
        for a,b in spans:
            print('---')
            for i in range(a,b): print(f'{i+1:4} {lines[i]}')
    except Exception as e: print('FETCH ERROR',p,type(e).__name__,str(e))
PY

Repository: nextcloud/assistant

Length of output: 8967


Script executed:

python3 - <<'PY'
import urllib.request
root='https://raw.githubusercontent.com/nextcloud/context_agent/99936dd8c518b164aae8bdb12e965a640fe6f1fc/'
for p, ranges in {
'ex_app/lib/agent.py':[(1,125),(175,235),(235,298)],
'ex_app/lib/all_tools/files.py':[(320,370)],
'pyproject.toml':[(1,120)],
}.items():
    try:
        with urllib.request.urlopen(root+p, timeout=25) as r: lines=r.read().decode().splitlines()
        print('\n###',root+p)
        for a,b in ranges:
            for i in range(a-1,min(b,len(lines))): print(f'{i+1:4} {lines[i]}')
    except Exception as e: print('FETCH ERROR',p,type(e).__name__,str(e))
PY

Repository: nextcloud/assistant

Length of output: 14889


Web query:

Langchain Python ToolCall TypedDict name args id type optional langchain_core messages tool tool_calls

Result:

Sources consulted:
- ToolCall | langchain_core | LangChain Reference — https://reference.langchain.com/python/langchain-core/messages/tool/ToolCall
- ToolCall | langchain_core | LangChain Reference — https://reference.langchain.com/python/langchain-core/messages/content/ToolCall
- langchain/openwiki/tools.md at master · langchain-ai/langchain · GitHub — https://github.com/langchain-ai/langchain/blob/master/openwiki/tools.md
- langchain/libs/core/langchain_core/messages/content.py at master · langchain-ai/langchain · GitHub — https://github.com/langchain-ai/langchain/blob/master/libs/core/langchain_core/messages/content.py
- Tool Calling with LangChain — https://www.langchain.com/blog/tool-calling-with-langchain
- …and 26 more

Script executed:

python3 - <<'PY'
import urllib.request
root='https://raw.githubusercontent.com/nextcloud/context_agent/99936dd8c518b164aae8bdb12e965a640fe6f1fc/'
for p, needles in {
'ex_app/lib/nc_model.py':['tool_calls','tool_call','AIMessage'],
'ex_app/lib/graph.py':['dangerous_tools','interrupt','tool_calls','ToolNode'],
'ex_app/lib/all_tools/lib/decorator.py':['dangerous_tool'],
'ex_app/lib/tools.py':['dangerous','get_tools'],
'appinfo/info.xml':['nextcloud']
}.items():
    try:
        with urllib.request.urlopen(root+p, timeout=25) as r: lines=r.read().decode().splitlines()
        print('\n###',root+p)
        spans=[]
        for i,l in enumerate(lines):
            if any(n in l for n in needles):
                a=max(0,i-8); b=min(len(lines),i+15)
                if spans and a<=spans[-1][1]: spans[-1]=(spans[-1][0],max(b,spans[-1][1]))
                else: spans.append((a,b))
        for a,b in spans:
            print('---')
            for i in range(a,b): print(f'{i+1:4} {lines[i]}')
    except Exception as e: print('FETCH ERROR',p,type(e).__name__,str(e))
PY

Repository: nextcloud/assistant

Length of output: 19980


Warn when destructiveness is unknown.

With Context Agent 2.8.0, a pending file deletion has no destructive or reversibility field. The dialog therefore shows an info card without the content-loss indicator. Show an unknown-risk warning when destructiveness is missing instead of treating the action as non-destructive.

// actions that don't report a destructive state (older backends) are assumed to be non-destructive
for (const action of this.actions) {
if (action?.destructive !== false) {
if (action?.destructive !== undefined && action?.destructive !== null && action.destructive !== false) {

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.

I agree with CodeRabbit 😁 . Let's clarify what should be displayed when the fields are missing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The fields are only missing when using context_agent < v2.9.0 and in that case the behavior now is the same as it was before.

// determine how many items are affected by the pending actions
let itemCount = 0
for (const action of this.actions) {
if (Number.isFinite(action?.item_count)) {

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.

Should we cast to int? Right now it seems possible to render float counts.

…ings

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Marcel Klehr <mklehr@gmx.net>
…that the count is a lower bound

Assisted-by: ClaudeCode:claude-opus-5-5
Signed-off-by: Marcel Klehr <mklehr@gmx.net>

@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

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 23ad1f25-586f-4584-8814-518610d3f943
Commits

Reviewing files that changed from the base of the PR and between 534adfb and 243015f.

Files selected for processing: 1

  • src/components/ChattyLLM/AgencyConfirmation.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.

{{ irreversible.description }}
</p>
</NcPopover>
<NcPopover v-if="itemCount.itemCount > 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.

Minor · Functional Correctness · Quick win

Show the item-count indicator when exactly one item is affected and the count is a lower bound.

The condition itemCount.itemCount > 1 hides the popover when the summed count is 1. If the count is inexact, the real total can be higher. Examples are one action with item_count: 1 and no item_count_exact, or a batch where another action omits item_count. In those cases the user sees no count warning. Show the popover when the count is above 1, or when the count is at least 1 and not exact.

Proposed fix

--- "a/src/components/ChattyLLM/AgencyConfirmation.vue"
+++ "b/src/components/ChattyLLM/AgencyConfirmation.vue"
@@ -58,7 +58,7 @@
 							{{ irreversible.description }}
 						</p>
 					</NcPopover>
-					<NcPopover v-if="itemCount.itemCount > 1">
+					<NcPopover v-if="itemCount.itemCount > 1 || (itemCount.itemCount > 0 && !itemCount.exact)">
 						<template #trigger>
 							<NcButton variant="tertiary"
 								:title="itemCount.description"
Committable suggestion

IMPORTANT
Review the replacement and indentation, then test before committing.

Suggested change
<NcPopover v-if="itemCount.itemCount > 1">
<NcPopover v-if="itemCount.itemCount > 1 || (itemCount.itemCount > 0 && !itemCount.exact)">

@marcelklehr
marcelklehr merged commit 53d84cf into main Nov 10, 2026
10 checks passed
@marcelklehr
marcelklehr deleted the feat/agency-confirmation-reversibility-itemcount branch November 10, 2026 10:10
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.

2 participants