mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-16 18:25:17 +02:00
feat: redesign live-note sidebar with Objective / Last run / Details tabs
Flatten the panel to match the rest of the app's design language. Splits the surface into three tabs: - Objective: full-height markdown render of the objective, in-tab plain monospace editor (no card-in-card chrome). - Last run: fetches via `runs:fetch` and shows the agent's full transcript — summary at top, then a compact chat of user/assistant turns with collapsible tool calls (Parameters/Result). - Details: triggers (single cron + windows + events with display/edit toggle) and collapsed Advanced (model/provider/danger zone) ending in "Convert to static note →". Adds a 2-column status strip (Last run · Triggers) above the tabs and a context-aware footer. Adopts the app's signature `uppercase tracking-wider text-muted-foreground` label style; drops nested bordered cards. New helper `lib/run-to-conversation.ts` converts `Run.log` events into ConversationItems for read-only playback — adapted from App.tsx's live converter, trimmed for static history (no streaming/permission flows, skips lifecycle and system/tool-role messages). Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
e3d2a0988b
commit
ab23cb4543
2 changed files with 801 additions and 324 deletions
File diff suppressed because it is too large
Load diff
138
apps/x/apps/renderer/src/lib/run-to-conversation.ts
Normal file
138
apps/x/apps/renderer/src/lib/run-to-conversation.ts
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
import type z from 'zod'
|
||||
import type { RunEvent } from '@x/shared/dist/runs.js'
|
||||
import {
|
||||
type ChatMessage,
|
||||
type ConversationItem,
|
||||
type ToolCall,
|
||||
normalizeToolInput,
|
||||
} from './chat-conversation'
|
||||
|
||||
type RunLog = z.infer<typeof RunEvent>[]
|
||||
|
||||
/**
|
||||
* Convert a closed Run.log into a flat list of ConversationItems suitable
|
||||
* for read-only playback. Adapted from App.tsx's live-streaming converter
|
||||
* (lines ~1731-1843) but trimmed for static history:
|
||||
*
|
||||
* - drops llm-stream-event (reasoning lands in the final message)
|
||||
* - drops run-processing-* / start / spawn-subflow (lifecycle, not content)
|
||||
* - drops system/tool-role messages (only user + assistant surface)
|
||||
* - drops permission/ask-human (live-only flows)
|
||||
*/
|
||||
export function runLogToConversation(log: RunLog): ConversationItem[] {
|
||||
const items: ConversationItem[] = []
|
||||
const toolCallMap = new Map<string, ToolCall>()
|
||||
|
||||
for (const event of log) {
|
||||
switch (event.type) {
|
||||
case 'message': {
|
||||
const msg = event.message
|
||||
if (msg.role !== 'user' && msg.role !== 'assistant') break
|
||||
|
||||
let textContent = ''
|
||||
let msgAttachments: ChatMessage['attachments']
|
||||
if (typeof msg.content === 'string') {
|
||||
textContent = msg.content
|
||||
} else if (Array.isArray(msg.content)) {
|
||||
const parts = msg.content as Array<{
|
||||
type: string
|
||||
text?: string
|
||||
path?: string
|
||||
filename?: string
|
||||
mimeType?: string
|
||||
size?: number
|
||||
toolCallId?: string
|
||||
toolName?: string
|
||||
arguments?: unknown
|
||||
}>
|
||||
|
||||
textContent = parts
|
||||
.filter((p) => p.type === 'text')
|
||||
.map((p) => p.text ?? '')
|
||||
.join('')
|
||||
|
||||
const attachmentParts = parts.filter((p) => p.type === 'attachment' && p.path)
|
||||
if (attachmentParts.length > 0) {
|
||||
msgAttachments = attachmentParts.map((p) => ({
|
||||
path: p.path!,
|
||||
filename: p.filename || p.path!.split('/').pop() || p.path!,
|
||||
mimeType: p.mimeType || 'application/octet-stream',
|
||||
size: p.size,
|
||||
}))
|
||||
}
|
||||
|
||||
if (msg.role === 'assistant') {
|
||||
for (const part of parts) {
|
||||
if (part.type === 'tool-call' && part.toolCallId && part.toolName) {
|
||||
const toolCall: ToolCall = {
|
||||
id: part.toolCallId,
|
||||
name: part.toolName,
|
||||
input: normalizeToolInput(part.arguments as ToolCall['input']),
|
||||
status: 'pending',
|
||||
timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(),
|
||||
}
|
||||
toolCallMap.set(toolCall.id, toolCall)
|
||||
items.push(toolCall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (textContent || msgAttachments) {
|
||||
items.push({
|
||||
id: event.messageId,
|
||||
role: msg.role,
|
||||
content: textContent,
|
||||
attachments: msgAttachments,
|
||||
timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(),
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tool-invocation': {
|
||||
const existing = event.toolCallId ? toolCallMap.get(event.toolCallId) : null
|
||||
if (existing) {
|
||||
existing.input = normalizeToolInput(event.input)
|
||||
existing.status = 'running'
|
||||
} else {
|
||||
const toolCall: ToolCall = {
|
||||
id: event.toolCallId || `tool-${items.length}`,
|
||||
name: event.toolName,
|
||||
input: normalizeToolInput(event.input),
|
||||
status: 'running',
|
||||
timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(),
|
||||
}
|
||||
if (event.toolCallId) toolCallMap.set(toolCall.id, toolCall)
|
||||
items.push(toolCall)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'tool-result': {
|
||||
const existing = event.toolCallId ? toolCallMap.get(event.toolCallId) : null
|
||||
if (existing) {
|
||||
existing.result = event.result
|
||||
existing.status = 'completed'
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'error': {
|
||||
items.push({
|
||||
id: `error-${items.length}`,
|
||||
kind: 'error',
|
||||
message: event.error,
|
||||
timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(),
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// Everything else is lifecycle/streaming — not part of the rendered transcript.
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue