ui: show permission-blocked tool calls in timeline
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user