Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/prcheck.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ jobs:

- name: Test renderer
run: pnpm run test:renderer
env:
# Reused jsdom workers accumulate heap across suites and breach the
# 4GB Node default on 16GB runners; 6GB keeps two workers in budget.
NODE_OPTIONS: --max-old-space-size=6144

build:
runs-on: ubuntu-24.04
Expand Down
16 changes: 16 additions & 0 deletions src/renderer/src/components/message/LiveDelegationToolCallCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@
{{ slotId }}
</p>
</div>
<span
v-if="permissionStatus"
data-testid="tool-call-permission-badge"
:data-permission-status="permissionStatus"
:class="[
'shrink-0 rounded border px-1.5 py-0.5 text-[10px] font-medium',
permissionStatus === 'granted'
? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
: 'border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300'
]"
>
{{
permissionStatus === 'granted' ? t('toolCall.badge.allowed') : t('toolCall.badge.denied')
}}
</span>
<span
class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium"
:class="statusBadgeClass"
Expand Down Expand Up @@ -98,6 +113,7 @@ const props = defineProps<{
detailsId: string
detailsExpanded: boolean
readOnly?: boolean
permissionStatus?: 'granted' | 'denied'
}>()

const emit = defineEmits<{ toggleDetails: [] }>()
Expand Down
25 changes: 23 additions & 2 deletions src/renderer/src/components/message/MessageBlockAction.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,24 @@
{{ t('components.messageBlockAction.continue') }}
</DcButton>
<div
v-if="!block.extra?.needContinue && block.action_type !== 'rate_limit'"
v-if="resolvedPermissionStatus"
data-testid="permission-resolved-label"
:data-permission-status="resolvedPermissionStatus"
class="text-xs flex flex-row gap-2 items-center"
:class="resolvedPermissionStatus === 'granted' ? 'text-emerald-600' : 'text-red-500'"
>
<Icon
:icon="resolvedPermissionStatus === 'granted' ? 'lucide:shield-check' : 'lucide:shield-x'"
class="w-4 h-4"
/>
{{
resolvedPermissionStatus === 'granted'
? t('components.messageBlockPermissionRequest.granted')
: t('components.messageBlockPermissionRequest.denied')
}}
</div>
<div
v-else-if="!block.extra?.needContinue && block.action_type !== 'rate_limit'"
class="text-xs text-gray-500 flex flex-row gap-2 items-center"
>
<Icon icon="lucide:check" class="w-4 h-4" />{{ t('components.messageBlockAction.continued') }}
Expand All @@ -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()

Expand All @@ -67,6 +87,7 @@ const progressTimer = ref<number | null>(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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
"
/>
<MessageBlockSearch
v-else-if="block.type === 'search'"
Expand All @@ -69,7 +72,8 @@ import { Icon } from '@iconify/vue'
import { useI18n } from 'vue-i18n'
import type {
DisplayAssistantMessageBlock,
DisplayMessageUsage
DisplayMessageUsage,
ResolvedPermissionStatus
} from '@/features/chat-page/model/displayMessage'
import { formatActivityDuration } from './messageActivityGroups'
import MessageBlockThink from './MessageBlockThink.vue'
Expand All @@ -85,6 +89,7 @@ const props = defineProps<{
reasoningCount: number
toolCallCount: number
readOnly?: boolean
permissionStatusByToolCallId?: Record<string, ResolvedPermissionStatus>
}>()

const emit = defineEmits<{
Expand Down
17 changes: 17 additions & 0 deletions src/renderer/src/components/message/MessageBlockToolCall.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
:details-id="detailsId"
:details-expanded="isExpanded"
:read-only="readOnly"
:permission-status="permissionStatus"
@toggle-details="toggleExpanded"
/>
<button
Expand Down Expand Up @@ -43,6 +44,21 @@
{{ summaryText }}
</span>
</div>
<span
Comment thread
zhangmo8 marked this conversation as resolved.
v-if="permissionStatus"
data-testid="tool-call-permission-badge"
:data-permission-status="permissionStatus"
:class="[
'shrink-0 rounded border px-1.5 py-0.5 text-[10px] font-medium',
permissionStatus === 'granted'
? 'border-emerald-500/20 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
: 'border-red-500/20 bg-red-500/10 text-red-700 dark:text-red-300'
]"
>
{{
permissionStatus === 'granted' ? t('toolCall.badge.allowed') : t('toolCall.badge.denied')
}}
</span>
<span
v-if="showRtkBadge"
data-testid="tool-call-rtk-badge"
Expand Down Expand Up @@ -263,6 +279,7 @@ const props = defineProps<{
threadId?: string
readOnly?: boolean
renderMode?: 'full' | 'tool-only' | 'app-only'
permissionStatus?: 'granted' | 'denied'
}>()

type ExpansionSource = 'auto' | 'manual' | null
Expand Down
24 changes: 23 additions & 1 deletion src/renderer/src/components/message/MessageItemAssistant.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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"
/>
<MessageBlockToolCall
Expand Down Expand Up @@ -88,6 +89,11 @@
:thread-id="currentThreadId"
:read-only="isReadOnly"
:render-mode="item.block.tool_call?.mcpResult?.app ? 'tool-only' : 'full'"
:permission-status="
item.block.tool_call?.id
? permissionStatusByToolCallId[item.block.tool_call.id]
: undefined
"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
/>
<MessageBlockQuestionRequest
v-else-if="
Expand Down Expand Up @@ -217,7 +223,9 @@ import { ref, computed, watch } from 'vue'
import {
type DisplayAssistantMessage,
type DisplayAssistantMessageBlock,
buildResolvedPermissionStatusByToolCallId,
filterRenderableAssistantBlocks,
getResolvedPermissionStatus,
isInternalAssistantToolCallBlock
} from '@/features/chat-page/model/displayMessage'
import MessageBlockContent from './MessageBlockContent.vue'
Expand Down Expand Up @@ -437,9 +445,23 @@ const shouldGroupActivity = computed(() => {
return currentMessage.value.status !== 'pending'
})

const permissionStatusByToolCallId = computed(() =>
buildResolvedPermissionStatusByToolCallId(currentContent.value)
)

// Resolved permission outcomes merge into their tool card; the standalone
// action card only remains as a fallback when the tool card is missing.
const currentVisibleContent = computed(() =>
currentContent.value.filter((block) => {
const status = getResolvedPermissionStatus(block)
const toolCallId = block.tool_call?.id
return !(status && toolCallId && permissionStatusByToolCallId.value[toolCallId])
})
)

const currentRenderItems = computed(() =>
buildAssistantRenderItems({
blocks: currentContent.value,
blocks: currentVisibleContent.value,
messageId: currentMessage.value.id,
messageUpdatedAt: currentMessage.value.updatedAt,
shouldGroup: shouldGroupActivity.value,
Expand Down
37 changes: 37 additions & 0 deletions src/renderer/src/features/chat-page/model/displayMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,43 @@ export function isInternalAssistantToolCallBlock(block: DisplayAssistantMessageB
)
}

export type ResolvedPermissionStatus = 'granted' | 'denied'

export function getResolvedPermissionStatus(
block: DisplayAssistantMessageBlock
): ResolvedPermissionStatus | null {
if (block.type !== 'action' || block.action_type !== 'tool_call_permission') {
return null
}
return block.status === 'granted' || block.status === 'denied' ? block.status : null
}

/**
* Maps tool call ids to their broker-resolved permission outcome, restricted to
* permission blocks whose tool card exists in the same block list so the outcome
* can be merged into that card instead of rendering a separate action card.
*/
export function buildResolvedPermissionStatusByToolCallId(
blocks: DisplayAssistantMessageBlock[]
): Record<string, ResolvedPermissionStatus> {
const toolCallIds = new Set<string>()
for (const block of blocks) {
if (block.type === 'tool_call' && block.tool_call?.id) {
toolCallIds.add(block.tool_call.id)
}
}

const statusByToolCallId: Record<string, ResolvedPermissionStatus> = {}
for (const block of blocks) {
const status = getResolvedPermissionStatus(block)
const toolCallId = block.tool_call?.id
if (status && toolCallId && toolCallIds.has(toolCallId)) {
statusByToolCallId[toolCallId] = status
}
}
return statusByToolCallId
}

export function isRenderableAssistantBlock(block: DisplayAssistantMessageBlock): boolean {
if (block.type === 'plan') {
return false
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/da-DK/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"title": "Værktøjskald",
"terminalOutput": "Terminaludgang",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "Tilladt",
"denied": "Afvist"
},
"replacementsCount": "Udført {count} erstatninger"
}
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/de-DE/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"imagePreviewCount": "Keine Bildvorschau | {count} Bildvorschau | {count} Bildvorschauen",
"terminalOutput": "Terminalausgabe",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "Erlaubt",
"denied": "Abgelehnt"
},
"replacementsCount": "{count} Ersetzungen abgeschlossen",
"fileOperation": "Dateivorgang",
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/en-US/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"imagePreviewCount": "No image previews | One image preview | {count} image previews",
"terminalOutput": "Terminal Output",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "Allowed",
Comment thread
zhangmo8 marked this conversation as resolved.
"denied": "Denied"
},
"replacementsCount": "Completed {count} replacements",
"fileOperation": "File Operation",
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/es-ES/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"imagePreviewCount": "No hay vistas previas de imágenes | Vista previa de una imagen | {count} vistas previas de imágenes",
"terminalOutput": "Salida terminal",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "Permitido",
"denied": "Denegado"
},
"replacementsCount": "Reemplazos {count} completados",
"fileOperation": "Operación de archivos",
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/fa-IR/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"title": "تماس ابزار",
"terminalOutput": "خروجی ترمینال",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "مجاز شد",
"denied": "رد شد"
},
"replacementsCount": "{count} مورد جایگزین شد"
}
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/fr-FR/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"title": "Appel d'outil",
"terminalOutput": "Sortie terminale",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "Autorisé",
"denied": "Refusé"
},
"replacementsCount": "Terminé {count} remplacement(s)"
}
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/he-IL/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"success": "הַצלָחָה",
"terminalOutput": "פלט מסוף",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "אושר",
"denied": "נדחה"
},
"replacementsCount": "הושלמו {count} החלפות"
}
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/id-ID/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"imagePreviewCount": "Tidak ada pratinjau gambar | {count} pratinjau gambar | {count} pratinjau gambar",
"terminalOutput": "Keluaran terminal",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "Diizinkan",
"denied": "Ditolak"
},
"replacementsCount": "Penggantian selesai di {count}",
"fileOperation": "Operasi berkas",
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/it-IT/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"imagePreviewCount": "Nessuna anteprima immagine | {count} anteprima immagine | {count} anteprime immagine",
"terminalOutput": "Output terminale",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "Consentito",
"denied": "Negato"
},
"replacementsCount": "{count} sostituzioni completate",
"fileOperation": "Operazione file",
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/ja-JP/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"title": "ツール呼び出し",
"terminalOutput": "端子出力",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "許可済み",
"denied": "拒否済み"
},
"replacementsCount": "{count}か所の置換を完了しました"
}
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/ko-KR/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
"title": "도구 호출",
"terminalOutput": "터미널 출력",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "허용됨",
"denied": "거부됨"
},
"replacementsCount": "{count}개 치환 완료"
}
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/ms-MY/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"imagePreviewCount": "Tiada pratonton gambar | Pratonton gambar {count} | Pratonton gambar {count}",
"terminalOutput": "Keluaran terminal",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "Dibenarkan",
"denied": "Ditolak"
},
"replacementsCount": "Selesai penggantian di {count}",
"fileOperation": "Operasi fail",
Expand Down
4 changes: 3 additions & 1 deletion src/renderer/src/i18n/pl-PL/toolCall.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
"imagePreviewCount": "Brak podglądów obrazów | Jeden podgląd obrazu | {count} podglądy obrazów | {count} podglądów obrazów",
"terminalOutput": "Wyjście terminala",
"badge": {
"rtk": "RTK"
"rtk": "RTK",
"allowed": "Zezwolono",
"denied": "Odmówiono"
},
"replacementsCount": "Zakończono wymianę {count}",
"fileOperation": "Operacja na pliku",
Expand Down
Loading