Files
BrowserOS/packages/browseros-agent/apps/agent/entrypoints/sidepanel/index/getMessageSegments.ts
Nikhil Sonti 3c629c5929 feat: tool approvals, governance dashboard, and execution history
- Add tool approval system with per-category approval configuration
- Build unified Governance dashboard (renamed from Admin) with pending
  approvals view and execution audit log
- Move execution history tracking into the app shell
- Extract buildChatRequestBody helper and add newtab system prompt
- Add approval config change detection for mid-conversation rebuilds
2026-04-13 09:43:30 -07:00

145 lines
3.9 KiB
TypeScript

import type { UIMessage } from 'ai'
export type ToolInvocationState =
| 'partial-call'
| 'call'
| 'result'
| 'input-streaming'
| 'input-available'
| 'output-available'
| 'output-error'
| 'approval-requested'
| 'approval-responded'
| 'output-denied'
export interface ToolInvocationInfo {
state: ToolInvocationState
toolCallId: string
toolName: string
input: Record<string, unknown>
output: unknown[]
approval?: { id: string; approved?: boolean; reason?: string }
}
export type NudgeType = 'schedule_suggestion' | 'app_connection'
export interface NudgeData {
type: NudgeType
[key: string]: unknown
}
export type MessageSegment =
| { type: 'text'; key: string; text: string }
| { type: 'reasoning'; key: string; text: string; isStreaming: boolean }
| { type: 'tool-batch'; key: string; tools: ToolInvocationInfo[] }
| { type: 'nudge'; key: string; nudgeType: NudgeType; data: NudgeData }
const NUDGE_TOOLS = new Set(['suggest_schedule', 'suggest_app_connection'])
function parseNudgeOutput(output: unknown): NudgeData | null {
try {
// output is { content: [{ type: "text", text: "JSON..." }], isError: false }
const result = output as {
content?: Array<{ type: string; text?: string }>
isError?: boolean
}
if (result?.isError) return null
const text = result?.content?.find((c) => c.type === 'text')?.text
if (!text) return null
const parsed = JSON.parse(text)
if (
parsed?.type === 'schedule_suggestion' ||
parsed?.type === 'app_connection'
) {
return parsed as NudgeData
}
} catch {
// ignore parse errors
}
return null
}
export const getMessageSegments = (
message: UIMessage,
isLastMessage: boolean,
isStreaming: boolean,
): MessageSegment[] => {
const segments: MessageSegment[] = []
let currentToolBatch: ToolInvocationInfo[] = []
let textSegmentCount = 0
let reasoningSegmentCount = 0
const flushToolBatch = () => {
if (currentToolBatch.length > 0) {
segments.push({
type: 'tool-batch',
key: `${message.id}-tools-${currentToolBatch[0].toolCallId}`,
tools: [...currentToolBatch],
})
currentToolBatch = []
}
}
for (let i = 0; i < message.parts.length; i++) {
const part = message.parts[i]
if (part.type === 'text') {
flushToolBatch()
segments.push({
type: 'text',
key: `${message.id}-text-${textSegmentCount}`,
text: part.text,
})
textSegmentCount++
} else if (part.type === 'reasoning') {
flushToolBatch()
segments.push({
type: 'reasoning',
key: `${message.id}-reasoning-${reasoningSegmentCount}`,
text: part.text,
isStreaming:
isStreaming && i === message.parts.length - 1 && isLastMessage,
})
reasoningSegmentCount++
} else if (part.type?.startsWith('tool-')) {
const toolPart = part as {
toolCallId: string
type: string
state: ToolInvocationState
input: Record<string, unknown>
output: unknown
approval?: { id: string; approved?: boolean; reason?: string }
}
const toolName = toolPart.type?.replace('tool-', '')
if (NUDGE_TOOLS.has(toolName) && toolPart.state === 'output-available') {
flushToolBatch()
const nudgeData = parseNudgeOutput(toolPart.output)
if (nudgeData) {
segments.push({
type: 'nudge',
key: `${message.id}-nudge-${toolPart.toolCallId}`,
nudgeType: nudgeData.type,
data: nudgeData,
})
}
} else if (!NUDGE_TOOLS.has(toolName)) {
currentToolBatch.push({
state: toolPart.state,
toolCallId: toolPart.toolCallId,
toolName,
input: toolPart?.input ?? {},
output: (toolPart?.output as unknown[]) ?? [],
approval: toolPart?.approval,
})
}
}
}
flushToolBatch()
return segments
}