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

View File

@@ -146,6 +146,30 @@
background-color: var(--surface-secondary); 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 { .message-timeline-compaction-auto {
border-color: var(--session-status-compacting-fg); border-color: var(--session-status-compacting-fg);
background-color: var(--surface-secondary); background-color: var(--surface-secondary);