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" />