ui: show permission-blocked tool calls in timeline

This commit is contained in:
Shantur Rathore
2026-01-08 19:16:39 +00:00
parent ff6d6f4f76
commit fd464f349a
2 changed files with 50 additions and 4 deletions

View File

@@ -5,7 +5,7 @@ import type { ClientPart } from "../types/message"
import type { MessageRecord } from "../stores/message-v2/types"
import { buildRecordDisplayData } from "../stores/message-v2/record-display-cache"
import { getToolIcon } from "./tool-call/utils"
import { User as UserIcon, Bot as BotIcon, FoldVertical } from "lucide-solid"
import { User as UserIcon, Bot as BotIcon, FoldVertical, ShieldAlert } from "lucide-solid"
export type TimelineSegmentType = "user" | "assistant" | "tool" | "compaction"
@@ -17,6 +17,7 @@ export interface TimelineSegment {
tooltip: string
shortLabel?: string
variant?: "auto" | "manual"
toolPartIds?: string[]
}
interface MessageTimelineProps {
@@ -47,6 +48,7 @@ interface PendingSegment {
toolTitles: string[]
toolTypeLabels: string[]
toolIcons: string[]
toolPartIds: string[]
hasPrimaryText: boolean
}
@@ -179,6 +181,7 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord)
label,
tooltip,
shortLabel,
toolPartIds: isToolSegment ? pending.toolPartIds : undefined,
})
segmentIndex += 1
pending = null
@@ -187,7 +190,7 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord)
const ensureSegment = (type: TimelineSegmentType): PendingSegment => {
if (!pending || pending.type !== type) {
flushPending()
pending = { type, texts: [], reasoningTexts: [], toolTitles: [], toolTypeLabels: [], toolIcons: [], hasPrimaryText: type !== "assistant" }
pending = { type, texts: [], reasoningTexts: [], toolTitles: [], toolTypeLabels: [], toolIcons: [], toolPartIds: [], hasPrimaryText: type !== "assistant" }
}
return pending!
}
@@ -204,6 +207,9 @@ export function buildTimelineSegments(instanceId: string, record: MessageRecord)
target.toolTitles.push(getToolTitle(toolPart))
target.toolTypeLabels.push(getToolTypeLabel(toolPart))
target.toolIcons.push(getToolIcon(typeof toolPart.tool === "string" ? toolPart.tool : "tool"))
if (typeof toolPart.id === "string" && toolPart.id.length > 0) {
target.toolPartIds.push(toolPart.id)
}
continue
}
@@ -359,9 +365,25 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
{(segment) => {
onCleanup(() => buttonRefs.delete(segment.id))
const isActive = () => props.activeMessageId === segment.messageId
const isHidden = () => segment.type === "tool" && !(showTools() || isActive())
const hasActivePermission = () => {
if (segment.type !== "tool") return false
const partIds = segment.toolPartIds ?? []
if (partIds.length === 0) return false
for (const partId of partIds) {
const permissionState = store().getPermissionState(segment.messageId, partId)
if (permissionState?.active) return true
}
return false
}
const isHidden = () => segment.type === "tool" && !(showTools() || isActive() || hasActivePermission())
const shortLabelContent = () => {
if (segment.type === "tool") {
if (hasActivePermission()) {
return <ShieldAlert class="message-timeline-icon" aria-hidden="true" />
}
return segment.shortLabel ?? getToolIcon("tool")
}
if (segment.type === "compaction") {
@@ -378,7 +400,7 @@ const MessageTimeline: Component<MessageTimelineProps> = (props) => {
ref={(el) => registerButtonRef(segment.id, el)}
type="button"
data-variant={segment.variant}
class={`message-timeline-segment message-timeline-${segment.type} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
class={`message-timeline-segment message-timeline-${segment.type} ${hasActivePermission() ? "message-timeline-segment-permission" : ""} ${segment.type === "compaction" ? `message-timeline-compaction-${segment.variant ?? "manual"}` : ""} ${isActive() ? "message-timeline-segment-active" : ""} ${isHidden() ? "message-timeline-segment-hidden" : ""}`}
aria-current={isActive() ? "true" : undefined}
aria-hidden={isHidden() ? "true" : undefined}

View File

@@ -146,6 +146,30 @@
background-color: var(--surface-secondary);
}
.message-timeline-segment.message-timeline-segment-permission {
border-color: var(--session-status-permission-fg);
background-color: var(--session-status-permission-bg);
color: var(--session-status-permission-fg);
}
.message-timeline-segment.message-timeline-segment-permission:hover,
.message-timeline-segment.message-timeline-segment-permission:focus-visible {
background-color: var(--session-status-permission-bg);
color: var(--session-status-permission-fg);
border-color: var(--session-status-permission-fg);
transform: translateY(-1px);
}
.message-timeline-segment-active.message-timeline-segment-permission,
.message-timeline-segment-active.message-timeline-segment-permission:hover,
.message-timeline-segment-active.message-timeline-segment-permission:focus-visible {
background-color: var(--session-status-permission-bg) !important;
border-color: var(--session-status-permission-fg) !important;
color: var(--session-status-permission-fg) !important;
transform: none;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35);
}
.message-timeline-compaction-auto {
border-color: var(--session-status-compacting-fg);
background-color: var(--surface-secondary);