feat: Auto-open PromptBar on permission request and improve permission card UI
PromptBar now auto-opens when a permission request arrives and it's hidden. Permission cards show rich contextual info: tool badge, description, code blocks for Bash, diff preview for Edit, file paths for Write, and icons on Allow/Deny buttons.
This commit is contained in:
@@ -395,13 +395,28 @@ function addPermissionCard(msg: any) {
|
||||
if (messages.some(m => m.intervention?.requestId === rid)) return
|
||||
|
||||
const input = msg.tool_input || {}
|
||||
const toolName = msg.tool_name || 'Unknown'
|
||||
|
||||
// Build a summary line for the content field (fallback)
|
||||
let detail = ''
|
||||
if (msg.tool_name === 'Bash') {
|
||||
if (toolName === 'Bash') {
|
||||
detail = input.command || ''
|
||||
} else if (msg.tool_name === 'Edit' || msg.tool_name === 'Write') {
|
||||
} else if (toolName === 'Edit') {
|
||||
detail = input.file_path || ''
|
||||
} else if (toolName === 'Write') {
|
||||
detail = input.file_path || ''
|
||||
} else if (toolName === 'Grep') {
|
||||
detail = `${input.pattern || ''} in ${input.path || '.'}`
|
||||
} else if (toolName === 'Glob') {
|
||||
detail = input.pattern || ''
|
||||
} else if (toolName === 'WebFetch') {
|
||||
detail = input.url || ''
|
||||
} else if (toolName === 'WebSearch') {
|
||||
detail = input.query || ''
|
||||
} else if (toolName.startsWith('mcp__')) {
|
||||
detail = JSON.stringify(input).slice(0, 300)
|
||||
} else {
|
||||
detail = JSON.stringify(input).slice(0, 200)
|
||||
detail = JSON.stringify(input).slice(0, 300)
|
||||
}
|
||||
|
||||
messages.push({
|
||||
@@ -412,7 +427,7 @@ function addPermissionCard(msg: any) {
|
||||
intervention: {
|
||||
type: 'permission',
|
||||
requestId: rid,
|
||||
toolName: msg.tool_name,
|
||||
toolName,
|
||||
toolInput: input,
|
||||
resolved: false
|
||||
}
|
||||
@@ -420,6 +435,36 @@ function addPermissionCard(msg: any) {
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
// ── Permission card helpers ──
|
||||
|
||||
function permToolMeta(toolName: string): ToolMeta {
|
||||
return TOOL_CATEGORIES[toolName] || getToolMeta(toolName)
|
||||
}
|
||||
|
||||
function permToolDescription(toolName: string): string {
|
||||
const descs: Record<string, string> = {
|
||||
Bash: 'Execute shell command',
|
||||
Edit: 'Modify file content',
|
||||
Write: 'Create or overwrite file',
|
||||
Read: 'Read file contents',
|
||||
Glob: 'Search for files',
|
||||
Grep: 'Search file contents',
|
||||
WebFetch: 'Fetch URL content',
|
||||
WebSearch: 'Web search',
|
||||
Task: 'Launch sub-agent',
|
||||
NotebookEdit: 'Edit Jupyter notebook',
|
||||
}
|
||||
if (descs[toolName]) return descs[toolName]
|
||||
if (toolName.startsWith('mcp__')) return 'MCP tool call'
|
||||
return 'Tool execution'
|
||||
}
|
||||
|
||||
function truncateMiddle(str: string, maxLen: number): string {
|
||||
if (!str || str.length <= maxLen) return str
|
||||
const half = Math.floor((maxLen - 3) / 2)
|
||||
return str.slice(0, half) + '...' + str.slice(-half)
|
||||
}
|
||||
|
||||
function addQuestionCard(msg: any) {
|
||||
const input = msg.tool_input || {}
|
||||
const questions = input.questions || []
|
||||
@@ -788,18 +833,78 @@ onBeforeUnmount(() => {
|
||||
<div class="intervention-card" :class="`intervention--${item.msg!.intervention.type}`">
|
||||
<!-- Permission card -->
|
||||
<template v-if="item.msg!.intervention.type === 'permission'">
|
||||
<div class="intv-header">
|
||||
<svg class="intv-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<div class="perm-header">
|
||||
<span
|
||||
class="perm-tool-badge"
|
||||
:style="{ '--badge-color': permToolMeta(item.msg!.intervention.toolName!).color }"
|
||||
>
|
||||
{{ permToolMeta(item.msg!.intervention.toolName!).icon }}
|
||||
</span>
|
||||
<div class="perm-header-text">
|
||||
<span class="perm-tool-name">{{ item.msg!.intervention.toolName }}</span>
|
||||
<span class="perm-tool-desc">{{ permToolDescription(item.msg!.intervention.toolName!) }}</span>
|
||||
</div>
|
||||
<svg class="perm-warn-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>
|
||||
<span class="intv-title">Permission: {{ item.msg!.intervention.toolName }}</span>
|
||||
</div>
|
||||
<div class="intv-detail">{{ item.msg!.content }}</div>
|
||||
|
||||
<!-- Bash: show description + command -->
|
||||
<template v-if="item.msg!.intervention.toolName === 'Bash'">
|
||||
<div v-if="(item.msg!.intervention.toolInput as any)?.description" class="perm-description">
|
||||
{{ (item.msg!.intervention.toolInput as any).description }}
|
||||
</div>
|
||||
<div class="perm-code-block">
|
||||
<span class="perm-code-prefix">$</span>
|
||||
<code class="perm-code">{{ (item.msg!.intervention.toolInput as any)?.command || '' }}</code>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Edit: show file + diff preview -->
|
||||
<template v-else-if="item.msg!.intervention.toolName === 'Edit'">
|
||||
<div class="perm-file-path">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
</svg>
|
||||
<span>{{ truncateMiddle((item.msg!.intervention.toolInput as any)?.file_path || '', 60) }}</span>
|
||||
</div>
|
||||
<div v-if="(item.msg!.intervention.toolInput as any)?.old_string" class="perm-diff">
|
||||
<div class="perm-diff-remove">{{ truncateMiddle((item.msg!.intervention.toolInput as any).old_string, 120) }}</div>
|
||||
<div class="perm-diff-add">{{ truncateMiddle((item.msg!.intervention.toolInput as any).new_string || '', 120) }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Write: show file + content preview -->
|
||||
<template v-else-if="item.msg!.intervention.toolName === 'Write'">
|
||||
<div class="perm-file-path">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
|
||||
<polyline points="14 2 14 8 20 8"/>
|
||||
</svg>
|
||||
<span>{{ truncateMiddle((item.msg!.intervention.toolInput as any)?.file_path || '', 60) }}</span>
|
||||
</div>
|
||||
<div v-if="(item.msg!.intervention.toolInput as any)?.content" class="perm-code-block perm-code-block--sm">
|
||||
<code class="perm-code">{{ truncateMiddle((item.msg!.intervention.toolInput as any).content, 200) }}</code>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Other tools: show key params -->
|
||||
<template v-else>
|
||||
<div class="perm-detail-rich">{{ item.msg!.content }}</div>
|
||||
</template>
|
||||
|
||||
<div v-if="!item.msg!.intervention.resolved" class="intv-actions">
|
||||
<button class="intv-btn intv-btn--allow" @click="respondPermission(item.msg!.id, item.msg!.intervention.requestId!, 'allow')">Allow</button>
|
||||
<button class="intv-btn intv-btn--deny" @click="respondPermission(item.msg!.id, item.msg!.intervention.requestId!, 'deny')">Deny</button>
|
||||
<button class="intv-btn intv-btn--allow" @click="respondPermission(item.msg!.id, item.msg!.intervention.requestId!, 'allow')">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><polyline points="20 6 9 17 4 12"/></svg>
|
||||
Allow
|
||||
</button>
|
||||
<button class="intv-btn intv-btn--deny" @click="respondPermission(item.msg!.id, item.msg!.intervention.requestId!, 'deny')">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
Deny
|
||||
</button>
|
||||
</div>
|
||||
<div v-else class="intv-resolved" :class="item.msg!.intervention.decision === 'allow' ? 'resolved--allow' : 'resolved--deny'">
|
||||
{{ item.msg!.intervention.decision === 'allow' ? 'Allowed' : 'Denied' }}
|
||||
@@ -1276,9 +1381,173 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.intervention--permission {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border: 1px solid rgba(239, 68, 68, 0.2);
|
||||
animation: permission-pulse 2s ease-in-out infinite;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Permission card header */
|
||||
.perm-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.perm-tool-badge {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--badge-color) 18%, transparent);
|
||||
color: var(--badge-color);
|
||||
border: 1px solid color-mix(in srgb, var(--badge-color) 25%, transparent);
|
||||
}
|
||||
|
||||
.perm-header-text {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.perm-tool-name {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.perm-tool-desc {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.perm-warn-icon {
|
||||
flex-shrink: 0;
|
||||
color: rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
|
||||
/* Permission description */
|
||||
.perm-description {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Permission code block */
|
||||
.perm-code-block {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
|
||||
}
|
||||
|
||||
.perm-code-block--sm {
|
||||
max-height: 80px;
|
||||
}
|
||||
|
||||
.perm-code-prefix {
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||
font-size: 11px;
|
||||
color: rgba(52, 211, 153, 0.6);
|
||||
flex-shrink: 0;
|
||||
line-height: 1.5;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.perm-code {
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Permission file path */
|
||||
.perm-file-path {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||
font-size: 11px;
|
||||
color: rgba(34, 211, 238, 0.8);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.perm-file-path svg {
|
||||
flex-shrink: 0;
|
||||
color: rgba(34, 211, 238, 0.5);
|
||||
}
|
||||
|
||||
/* Permission diff preview */
|
||||
.perm-diff {
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
max-height: 100px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
|
||||
}
|
||||
|
||||
.perm-diff-remove,
|
||||
.perm-diff-add {
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Fira Code', monospace;
|
||||
font-size: 10px;
|
||||
padding: 4px 8px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.perm-diff-remove {
|
||||
background: rgba(239, 68, 68, 0.08);
|
||||
color: rgba(248, 113, 113, 0.8);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.perm-diff-remove::before {
|
||||
content: '- ';
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.perm-diff-add {
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
color: rgba(52, 211, 153, 0.8);
|
||||
}
|
||||
|
||||
.perm-diff-add::before {
|
||||
content: '+ ';
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Rich detail for other tools */
|
||||
.perm-detail-rich {
|
||||
font-size: 11px;
|
||||
font-family: 'SF Mono', monospace;
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 80px;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(255, 255, 255, 0.1) transparent;
|
||||
}
|
||||
|
||||
.intervention--question {
|
||||
@@ -1314,7 +1583,10 @@ onBeforeUnmount(() => {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 60px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.intv-actions {
|
||||
@@ -1324,13 +1596,16 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
|
||||
.intv-btn {
|
||||
padding: 4px 14px;
|
||||
padding: 5px 14px;
|
||||
border-radius: 6px;
|
||||
border: none;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.intv-btn--allow {
|
||||
|
||||
Reference in New Issue
Block a user