diff --git a/apps/x/apps/renderer/package.json b/apps/x/apps/renderer/package.json index 359f1709..a193b3f1 100644 --- a/apps/x/apps/renderer/package.json +++ b/apps/x/apps/renderer/package.json @@ -49,6 +49,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-tweet": "^3.2.2", "recharts": "^3.8.0", "remark-breaks": "^4.0.0", "sonner": "^2.0.7", diff --git a/apps/x/apps/renderer/src/components/live-note-sidebar.tsx b/apps/x/apps/renderer/src/components/live-note-sidebar.tsx index 5ad03873..3aa9cf2c 100644 --- a/apps/x/apps/renderer/src/components/live-note-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/live-note-sidebar.tsx @@ -1,16 +1,27 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { Streamdown } from 'streamdown' import '@/styles/live-note-panel.css' import { Button } from '@/components/ui/button' import { Switch } from '@/components/ui/switch' import { Textarea } from '@/components/ui/textarea' import { Input } from '@/components/ui/input' import { - Radio, Clock, Play, Square, Loader2, Sparkles, CalendarClock, Zap, - Trash2, AlertCircle, ChevronDown, ChevronUp, Plus, X, Save, + Play, Square, Loader2, Sparkles, + AlertCircle, Plus, X, Check, Pencil, Radio, Repeat, Clock, Zap, + ChevronDown, ChevronRight, } from 'lucide-react' import { LiveNoteSchema, type LiveNote, type Triggers } from '@x/shared/dist/live-note.js' +import type { Run } from '@x/shared/dist/runs.js' +import type z from 'zod' import { useLiveNoteAgentStatus } from '@/hooks/use-live-note-agent-status' import { formatRelativeTime } from '@/lib/relative-time' +import { + type ConversationItem, type ToolCall, + isChatMessage, isToolCall, isErrorMessage, + getToolDisplayName, toToolState, normalizeToolOutput, +} from '@/lib/chat-conversation' +import { runLogToConversation } from '@/lib/run-to-conversation' +import { Tool, ToolHeader, ToolContent, ToolTabbedContent } from '@/components/ai-elements/tool' export type OpenLiveNotePanelDetail = { filePath: string @@ -21,7 +32,7 @@ const CRON_PHRASES: Record = { '*/5 * * * *': 'Every 5 minutes', '*/15 * * * *': 'Every 15 minutes', '*/30 * * * *': 'Every 30 minutes', - '0 * * * *': 'Hourly', + '0 * * * *': 'Hourly, on the hour', '0 */2 * * *': 'Every 2 hours', '0 */6 * * *': 'Every 6 hours', '0 */12 * * *': 'Every 12 hours', @@ -38,46 +49,34 @@ function describeCron(expr: string): string { return CRON_PHRASES[expr.trim()] ?? expr } -function summarizeTriggers(live: LiveNote): { icon: 'timer' | 'calendar' | 'bolt'; text: string } { - const t = live.triggers - if (!t) return { icon: 'bolt', text: 'Manual only' } +function summarizeSchedule(triggers: Triggers | undefined): string { + if (!triggers) return 'Manual only' const parts: string[] = [] - if (t.cronExpr) parts.push(describeCron(t.cronExpr)) - if (t.windows && t.windows.length > 0) { - parts.push(t.windows.length === 1 - ? `${t.windows[0].startTime}–${t.windows[0].endTime}` - : `${t.windows.length} windows`) + if (triggers.cronExpr) parts.push(describeCron(triggers.cronExpr)) + if (triggers.windows && triggers.windows.length > 0) { + parts.push(triggers.windows.length === 1 + ? `${triggers.windows[0].startTime}–${triggers.windows[0].endTime}` + : `${triggers.windows.length} windows`) } - if (t.eventMatchCriteria) parts.push('event-driven') - if (parts.length === 0) return { icon: 'bolt', text: 'Manual only' } - const icon = t.cronExpr ? 'timer' : t.windows?.length ? 'calendar' : 'bolt' - return { icon, text: parts.join(' · ') } -} - -function ScheduleIcon({ icon, size = 14 }: { icon: 'timer' | 'calendar' | 'bolt'; size?: number }) { - if (icon === 'timer') return - if (icon === 'calendar') return - return + if (triggers.eventMatchCriteria) parts.push('events') + return parts.length === 0 ? 'Manual only' : parts.join(' · ') } function stripKnowledgePrefix(p: string): string { return p.replace(/^knowledge\//, '') } -function formatDateTime(iso: string | null | undefined): string { - if (!iso) return '' +function formatRunAt(iso: string): string { const d = new Date(iso) - return d.toLocaleString('en-US', { - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: '2-digit', - hour12: true, - }) + const date = d.toLocaleString('en-US', { month: 'short', day: 'numeric' }) + const time = d.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }) + return `${date} · ${time}` } const HH_MM = /^([01]\d|2[0-3]):[0-5]\d$/ +type Tab = 'objective' | 'last-run' | 'details' + export interface LiveNoteSidebarProps { /** * Note path the panel should bind to. Workspace-relative (`knowledge/Foo.md`) @@ -96,6 +95,9 @@ export function LiveNoteSidebar({ filePath, onClose }: LiveNoteSidebarProps) { const [saving, setSaving] = useState(false) const [confirmingDelete, setConfirmingDelete] = useState(false) const [error, setError] = useState(null) + const [tab, setTab] = useState('objective') + const [editingObjective, setEditingObjective] = useState(false) + const [editingEvents, setEditingEvents] = useState(false) const [showAdvanced, setShowAdvanced] = useState(false) const knowledgeRelPath = useMemo(() => stripKnowledgePrefix(filePath ?? ''), [filePath]) @@ -127,8 +129,10 @@ export function LiveNoteSidebar({ filePath, onClose }: LiveNoteSidebarProps) { } }, []) - // Reset transient panel state and reload data whenever the bound path changes. useEffect(() => { + setTab('objective') + setEditingObjective(false) + setEditingEvents(false) setShowAdvanced(false) setConfirmingDelete(false) setError(null) @@ -140,7 +144,6 @@ export function LiveNoteSidebar({ filePath, onClose }: LiveNoteSidebarProps) { } }, [knowledgeRelPath, refresh]) - // Re-fetch when a run completes for this file. useEffect(() => { if (!knowledgeRelPath) return const state = agentStatus.get(knowledgeRelPath) @@ -172,6 +175,8 @@ export function LiveNoteSidebar({ filePath, onClose }: LiveNoteSidebarProps) { } setLive(res.live ?? null) setDraft(res.live ? structuredClone(res.live) as LiveNote : null) + setEditingObjective(false) + setEditingEvents(false) } catch (err) { setError(err instanceof Error ? err.message : String(err)) } finally { @@ -179,6 +184,11 @@ export function LiveNoteSidebar({ filePath, onClose }: LiveNoteSidebarProps) { } }, [knowledgeRelPath, draft]) + const handleCancelObjective = useCallback(() => { + if (live) setDraft(d => d ? { ...d, objective: live.objective } : d) + setEditingObjective(false) + }, [live]) + const handleToggleActive = useCallback(async () => { if (!knowledgeRelPath || !live) return setSaving(true) @@ -250,30 +260,76 @@ export function LiveNoteSidebar({ filePath, onClose }: LiveNoteSidebarProps) { onClose() }, [filePath, onClose]) - const handleMakeLive = useCallback(() => { - // Empty-state CTA: hand off to Copilot for the natural-language flow. - handleEditWithCopilot() - }, [handleEditWithCopilot]) - if (!filePath) return null const noteTitle = filePath ? (filePath.split('/').pop() ?? filePath).replace(/\.md$/, '') : 'Live note' - const sched = live ? summarizeTriggers(live) : null const paused = live?.active === false - return ( -