From 069a389737ce2d21c6e1edf8da6464cc190adde1 Mon Sep 17 00:00:00 2001
From: zhangmo8
Date: Mon, 10 Aug 2026 17:37:18 +0800
Subject: [PATCH 1/5] fix(chat): render resolved permission outcomes
---
.../components/message/MessageBlockAction.vue | 25 +++++-
.../message/MessageBlockActivityGroup.vue | 7 +-
.../message/MessageBlockToolCall.vue | 16 ++++
.../message/MessageItemAssistant.vue | 24 +++++-
.../chat-page/model/displayMessage.ts | 37 +++++++++
src/renderer/src/i18n/da-DK/toolCall.json | 4 +-
src/renderer/src/i18n/de-DE/toolCall.json | 4 +-
src/renderer/src/i18n/en-US/toolCall.json | 4 +-
src/renderer/src/i18n/es-ES/toolCall.json | 4 +-
src/renderer/src/i18n/fa-IR/toolCall.json | 4 +-
src/renderer/src/i18n/fr-FR/toolCall.json | 4 +-
src/renderer/src/i18n/he-IL/toolCall.json | 4 +-
src/renderer/src/i18n/id-ID/toolCall.json | 4 +-
src/renderer/src/i18n/it-IT/toolCall.json | 4 +-
src/renderer/src/i18n/ja-JP/toolCall.json | 4 +-
src/renderer/src/i18n/ko-KR/toolCall.json | 4 +-
src/renderer/src/i18n/ms-MY/toolCall.json | 4 +-
src/renderer/src/i18n/pl-PL/toolCall.json | 4 +-
src/renderer/src/i18n/pt-BR/toolCall.json | 4 +-
src/renderer/src/i18n/ru-RU/toolCall.json | 4 +-
src/renderer/src/i18n/tr-TR/toolCall.json | 4 +-
src/renderer/src/i18n/vi-VN/toolCall.json | 4 +-
src/renderer/src/i18n/zh-CN/toolCall.json | 4 +-
src/renderer/src/i18n/zh-HK/toolCall.json | 4 +-
src/renderer/src/i18n/zh-TW/toolCall.json | 4 +-
.../message/MessageBlockBasics.test.ts | 70 ++++++++++++++++
.../message/MessageBlockToolCall.test.ts | 37 +++++++++
.../message/MessageItemAssistant.test.ts | 83 +++++++++++++++++++
28 files changed, 355 insertions(+), 24 deletions(-)
diff --git a/src/renderer/src/components/message/MessageBlockAction.vue b/src/renderer/src/components/message/MessageBlockAction.vue
index 347229e0d3..0993505ab4 100644
--- a/src/renderer/src/components/message/MessageBlockAction.vue
+++ b/src/renderer/src/components/message/MessageBlockAction.vue
@@ -35,7 +35,24 @@
{{ t('components.messageBlockAction.continue') }}
+
+ {{
+ resolvedPermissionStatus === 'granted'
+ ? t('components.messageBlockPermissionRequest.granted')
+ : t('components.messageBlockPermissionRequest.denied')
+ }}
+
+
{{ t('components.messageBlockAction.continued') }}
@@ -48,7 +65,10 @@ import { useI18n } from 'vue-i18n'
import { Icon } from '@iconify/vue'
import { DcButton } from '@dc-ui/components/button'
import { computed, ref, onMounted, onUnmounted } from 'vue'
-import type { DisplayAssistantMessageBlock } from '@/features/chat-page/model/displayMessage'
+import {
+ type DisplayAssistantMessageBlock,
+ getResolvedPermissionStatus
+} from '@/features/chat-page/model/displayMessage'
const { t } = useI18n()
@@ -67,6 +87,7 @@ const progressTimer = ref(null)
const currentTime = ref(Date.now())
const isReadOnly = computed(() => props.isReadOnly === true)
const isRateLimitBlock = computed(() => props.block.action_type === 'rate_limit')
+const resolvedPermissionStatus = computed(() => getResolvedPermissionStatus(props.block))
const elapsedSeconds = computed(() => {
if (!isRateLimitBlock.value) {
return 0
diff --git a/src/renderer/src/components/message/MessageBlockActivityGroup.vue b/src/renderer/src/components/message/MessageBlockActivityGroup.vue
index 381957e6f1..45810cce90 100644
--- a/src/renderer/src/components/message/MessageBlockActivityGroup.vue
+++ b/src/renderer/src/components/message/MessageBlockActivityGroup.vue
@@ -51,6 +51,9 @@
:thread-id="threadId"
:read-only="readOnly"
render-mode="tool-only"
+ :permission-status="
+ block.tool_call?.id ? permissionStatusByToolCallId?.[block.tool_call.id] : undefined
+ "
/>
}>()
const emit = defineEmits<{
diff --git a/src/renderer/src/components/message/MessageBlockToolCall.vue b/src/renderer/src/components/message/MessageBlockToolCall.vue
index f4e040b236..8082fda0c3 100644
--- a/src/renderer/src/components/message/MessageBlockToolCall.vue
+++ b/src/renderer/src/components/message/MessageBlockToolCall.vue
@@ -43,6 +43,21 @@
{{ summaryText }}
+
+ {{
+ permissionStatus === 'granted' ? t('toolCall.badge.allowed') : t('toolCall.badge.denied')
+ }}
+
()
type ExpansionSource = 'auto' | 'manual' | null
diff --git a/src/renderer/src/components/message/MessageItemAssistant.vue b/src/renderer/src/components/message/MessageItemAssistant.vue
index abb756075f..2e265abeb8 100644
--- a/src/renderer/src/components/message/MessageItemAssistant.vue
+++ b/src/renderer/src/components/message/MessageItemAssistant.vue
@@ -48,6 +48,7 @@
:reasoning-count="item.reasoningCount"
:tool-call-count="item.toolCallCount"
:read-only="isReadOnly"
+ :permission-status-by-tool-call-id="permissionStatusByToolCallId"
@toggle-collapse="handleCollapseToggle"
/>
{
expect(wrapper.emitted('continue')).toEqual([['s1', 'm1']])
})
+ const createPermissionBlock = (
+ status: DisplayAssistantMessageBlock['status'],
+ overrides: Partial = {}
+ ): DisplayAssistantMessageBlock =>
+ createBlock({
+ action_type: 'tool_call_permission',
+ status,
+ tool_call: { id: 'tc1', name: 'run_command' },
+ ...overrides
+ })
+
+ it('renders granted permission outcome instead of the generic continued label', () => {
+ const wrapper = mount(MessageBlockAction, {
+ props: {
+ messageId: 'm1',
+ conversationId: 's1',
+ block: createPermissionBlock('granted')
+ }
+ })
+
+ const label = wrapper.find('[data-testid="permission-resolved-label"]')
+ expect(label.exists()).toBe(true)
+ expect(label.attributes('data-permission-status')).toBe('granted')
+ expect(wrapper.text()).toContain('components.messageBlockPermissionRequest.granted')
+ expect(wrapper.text()).not.toContain('components.messageBlockAction.continued')
+ })
+
+ it('renders denied permission outcome without any success affordance', () => {
+ const wrapper = mount(MessageBlockAction, {
+ props: {
+ messageId: 'm1',
+ conversationId: 's1',
+ block: createPermissionBlock('denied', { content: 'User denied the request.' })
+ }
+ })
+
+ const label = wrapper.find('[data-testid="permission-resolved-label"]')
+ expect(label.attributes('data-permission-status')).toBe('denied')
+ expect(wrapper.text()).toContain('components.messageBlockPermissionRequest.denied')
+ expect(wrapper.text()).not.toContain('components.messageBlockAction.continued')
+ expect(wrapper.findAll('button')).toHaveLength(0)
+ })
+
+ it('keeps denied rendering identical in read-only history', () => {
+ const wrapper = mount(MessageBlockAction, {
+ props: {
+ messageId: 'm1',
+ conversationId: 's1',
+ isReadOnly: true,
+ block: createPermissionBlock('denied')
+ }
+ })
+
+ expect(
+ wrapper.find('[data-testid="permission-resolved-label"]').attributes('data-permission-status')
+ ).toBe('denied')
+ })
+
+ it('does not mark pending permission requests as resolved', () => {
+ const wrapper = mount(MessageBlockAction, {
+ props: {
+ messageId: 'm1',
+ conversationId: 's1',
+ block: createPermissionBlock('pending', { extra: { needsUserAction: true } })
+ }
+ })
+
+ expect(wrapper.find('[data-testid="permission-resolved-label"]').exists()).toBe(false)
+ })
+
it('renders a compact rate limit status block', () => {
const wrapper = mount(MessageBlockAction, {
props: {
diff --git a/test/renderer/components/message/MessageBlockToolCall.test.ts b/test/renderer/components/message/MessageBlockToolCall.test.ts
index f42994e077..8e7aac6828 100644
--- a/test/renderer/components/message/MessageBlockToolCall.test.ts
+++ b/test/renderer/components/message/MessageBlockToolCall.test.ts
@@ -1073,4 +1073,41 @@ describe('MessageBlockToolCall', () => {
expect(selectSessionMock).toHaveBeenCalledWith('child-alpha')
})
+
+ it('renders the resolved permission badge on the tool pill', () => {
+ const wrapper = mount(MessageBlockToolCall, {
+ props: {
+ block: createBlock(),
+ permissionStatus: 'granted'
+ }
+ })
+
+ const badge = wrapper.find('[data-testid="tool-call-permission-badge"]')
+ expect(badge.exists()).toBe(true)
+ expect(badge.attributes('data-permission-status')).toBe('granted')
+ expect(badge.text()).toBe('toolCall.badge.allowed')
+ })
+
+ it('renders a denied permission badge distinct from success styling', () => {
+ const wrapper = mount(MessageBlockToolCall, {
+ props: {
+ block: createBlock({ status: 'error' }),
+ permissionStatus: 'denied'
+ }
+ })
+
+ const badge = wrapper.find('[data-testid="tool-call-permission-badge"]')
+ expect(badge.attributes('data-permission-status')).toBe('denied')
+ expect(badge.text()).toBe('toolCall.badge.denied')
+ })
+
+ it('renders no permission badge when no permission outcome is associated', () => {
+ const wrapper = mount(MessageBlockToolCall, {
+ props: {
+ block: createBlock()
+ }
+ })
+
+ expect(wrapper.find('[data-testid="tool-call-permission-badge"]').exists()).toBe(false)
+ })
})
diff --git a/test/renderer/components/message/MessageItemAssistant.test.ts b/test/renderer/components/message/MessageItemAssistant.test.ts
index c9cf9be1a1..eb0f7dd42f 100644
--- a/test/renderer/components/message/MessageItemAssistant.test.ts
+++ b/test/renderer/components/message/MessageItemAssistant.test.ts
@@ -742,4 +742,87 @@ describe('MessageItemAssistant', () => {
expect(memoryActivity.openTurnMemories).not.toHaveBeenCalled()
})
+
+ describe('resolved permission projection', () => {
+ const createPermissionActionBlock = (
+ status: DisplayAssistantMessageBlock['status'],
+ toolCallId = 'tc1'
+ ): DisplayAssistantMessageBlock => ({
+ type: 'action',
+ action_type: 'tool_call_permission',
+ status,
+ timestamp: 1,
+ tool_call: { id: toolCallId, name: 'run_command' }
+ })
+
+ const ToolCallStub = defineComponent({
+ name: 'MessageBlockToolCall',
+ props: {
+ permissionStatus: {
+ type: String,
+ default: undefined
+ }
+ },
+ template:
+ ''
+ })
+
+ const mountWith = (content: DisplayAssistantMessageBlock[]) =>
+ mount(MessageItemAssistant, {
+ props: {
+ message: createMessage('pending', content),
+ isCapturingImage: false,
+ isInGeneratingThread: true
+ },
+ global: {
+ ...global,
+ stubs: {
+ ...global.stubs,
+ MessageBlockToolCall: ToolCallStub
+ }
+ }
+ })
+
+ it('merges the granted outcome into the tool card and hides the action card', () => {
+ const wrapper = mountWith([
+ createPermissionActionBlock('granted'),
+ createToolCallBlock({ tool_call: { id: 'tc1', name: 'run_command' } })
+ ])
+
+ expect(wrapper.findComponent({ name: 'MessageBlockAction' }).exists()).toBe(false)
+ expect(
+ wrapper.find('[data-testid="tool-call-stub"]').attributes('data-permission-status')
+ ).toBe('granted')
+ })
+
+ it('merges the denied outcome into the tool card', () => {
+ const wrapper = mountWith([
+ createPermissionActionBlock('denied'),
+ createToolCallBlock({ tool_call: { id: 'tc1', name: 'run_command' } })
+ ])
+
+ expect(wrapper.findComponent({ name: 'MessageBlockAction' }).exists()).toBe(false)
+ expect(
+ wrapper.find('[data-testid="tool-call-stub"]').attributes('data-permission-status')
+ ).toBe('denied')
+ })
+
+ it('keeps the standalone action card when the tool card is missing', () => {
+ const wrapper = mountWith([createPermissionActionBlock('denied', 'tc-missing')])
+
+ expect(wrapper.findComponent({ name: 'MessageBlockAction' }).exists()).toBe(true)
+ })
+
+ it('keeps pending permission action blocks visible and unmerged', () => {
+ const wrapper = mountWith([
+ createPermissionActionBlock('pending'),
+ createToolCallBlock({ tool_call: { id: 'tc1', name: 'run_command' } })
+ ])
+
+ expect(wrapper.findComponent({ name: 'MessageBlockAction' }).exists()).toBe(true)
+ expect(
+ wrapper.find('[data-testid="tool-call-stub"]').attributes('data-permission-status')
+ ).toBe('')
+ })
+ })
})
From 276c67eab5272aa607ef84e375282988b1104290 Mon Sep 17 00:00:00 2001
From: zhangmo8
Date: Mon, 10 Aug 2026 17:49:53 +0800
Subject: [PATCH 2/5] chore(icons): regenerate icon collections
---
src/renderer/src/lib/icons/icon-collections.generated.ts | 3 +++
src/renderer/src/lib/icons/icon-whitelist.generated.ts | 1 +
2 files changed, 4 insertions(+)
diff --git a/src/renderer/src/lib/icons/icon-collections.generated.ts b/src/renderer/src/lib/icons/icon-collections.generated.ts
index 46da191dbb..b605d879d8 100644
--- a/src/renderer/src/lib/icons/icon-collections.generated.ts
+++ b/src/renderer/src/lib/icons/icon-collections.generated.ts
@@ -649,6 +649,9 @@ export const lucideIconCollection = {
'shield-off': {
body: ''
},
+ 'shield-x': {
+ body: ''
+ },
'shopping-bag': {
body: ''
},
diff --git a/src/renderer/src/lib/icons/icon-whitelist.generated.ts b/src/renderer/src/lib/icons/icon-whitelist.generated.ts
index 043316e0b6..e10c84ebde 100644
--- a/src/renderer/src/lib/icons/icon-whitelist.generated.ts
+++ b/src/renderer/src/lib/icons/icon-whitelist.generated.ts
@@ -223,6 +223,7 @@ export const GENERATED_ICON_WHITELIST: Record
Date: Tue, 11 Aug 2026 11:31:14 +0800
Subject: [PATCH 3/5] fix(chat): render delegation permission outcome
---
.../message/LiveDelegationToolCallCard.vue | 16 ++++
.../message/MessageBlockToolCall.vue | 1 +
.../message/MessageBlockToolCall.test.ts | 77 +++++++++++++++++++
.../message/MessageItemAssistant.test.ts | 14 ++++
4 files changed, 108 insertions(+)
diff --git a/src/renderer/src/components/message/LiveDelegationToolCallCard.vue b/src/renderer/src/components/message/LiveDelegationToolCallCard.vue
index 13af5d6671..9bc83e9faa 100644
--- a/src/renderer/src/components/message/LiveDelegationToolCallCard.vue
+++ b/src/renderer/src/components/message/LiveDelegationToolCallCard.vue
@@ -14,6 +14,21 @@
{{ slotId }}
+
+ {{
+ permissionStatus === 'granted' ? t('toolCall.badge.allowed') : t('toolCall.badge.denied')
+ }}
+
()
const emit = defineEmits<{ toggleDetails: [] }>()
diff --git a/src/renderer/src/components/message/MessageBlockToolCall.vue b/src/renderer/src/components/message/MessageBlockToolCall.vue
index 8082fda0c3..76781e144e 100644
--- a/src/renderer/src/components/message/MessageBlockToolCall.vue
+++ b/src/renderer/src/components/message/MessageBlockToolCall.vue
@@ -8,6 +8,7 @@
:details-id="detailsId"
:details-expanded="isExpanded"
:read-only="readOnly"
+ :permission-status="permissionStatus"
@toggle-details="toggleExpanded"
/>