prompt_objects 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -0
- data/CLAUDE.md +112 -44
- data/README.md +5 -0
- data/frontend/index.html +5 -1
- data/frontend/src/App.tsx +70 -78
- data/frontend/src/canvas/CanvasView.tsx +5 -5
- data/frontend/src/canvas/constants.ts +31 -31
- data/frontend/src/canvas/inspector/InspectorPanel.tsx +4 -4
- data/frontend/src/canvas/inspector/POInspector.tsx +35 -35
- data/frontend/src/canvas/inspector/ToolCallInspector.tsx +13 -13
- data/frontend/src/canvas/nodes/PONode.ts +2 -2
- data/frontend/src/components/ContextMenu.tsx +5 -4
- data/frontend/src/components/Inspector.tsx +232 -0
- data/frontend/src/components/MarkdownMessage.tsx +22 -20
- data/frontend/src/components/MethodList.tsx +90 -0
- data/frontend/src/components/ModelSelector.tsx +13 -14
- data/frontend/src/components/NotificationPanel.tsx +29 -33
- data/frontend/src/components/ObjectList.tsx +78 -0
- data/frontend/src/components/PaneSlot.tsx +30 -0
- data/frontend/src/components/SourcePane.tsx +202 -0
- data/frontend/src/components/SystemBar.tsx +74 -0
- data/frontend/src/components/Transcript.tsx +76 -0
- data/frontend/src/components/UsagePanel.tsx +27 -27
- data/frontend/src/components/Workspace.tsx +260 -0
- data/frontend/src/components/index.ts +10 -9
- data/frontend/src/hooks/useResize.ts +55 -0
- data/frontend/src/hooks/useWebSocket.ts +28 -0
- data/frontend/src/index.css +28 -10
- data/frontend/src/store/index.ts +4 -0
- data/frontend/src/types/index.ts +2 -0
- data/frontend/tailwind.config.js +28 -9
- data/lib/prompt_objects/capability.rb +23 -1
- data/lib/prompt_objects/connectors/mcp.rb +2 -16
- data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +1 -6
- data/lib/prompt_objects/prompt_object.rb +126 -0
- data/lib/prompt_objects/server/api/routes.rb +3 -48
- data/lib/prompt_objects/server/public/assets/{index-xvyeb-5Z.js → index-D1myxE0l.js} +211 -211
- data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +1 -0
- data/lib/prompt_objects/server/public/index.html +7 -3
- data/lib/prompt_objects/server/websocket_handler.rb +23 -100
- data/lib/prompt_objects/server.rb +6 -62
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/primitives/find_objects.rb +1 -1
- data/templates/arc-agi-1/primitives/grid_diff.rb +2 -2
- data/templates/arc-agi-1/primitives/grid_info.rb +1 -1
- data/templates/arc-agi-1/primitives/grid_transform.rb +1 -1
- data/templates/arc-agi-1/primitives/render_grid.rb +1 -0
- data/templates/arc-agi-1/primitives/test_solution.rb +3 -0
- metadata +12 -13
- data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
- data/frontend/src/components/ChatPanel.tsx +0 -296
- data/frontend/src/components/Dashboard.tsx +0 -83
- data/frontend/src/components/Header.tsx +0 -153
- data/frontend/src/components/MessageBus.tsx +0 -56
- data/frontend/src/components/POCard.tsx +0 -56
- data/frontend/src/components/PODetail.tsx +0 -124
- data/frontend/src/components/PromptPanel.tsx +0 -156
- data/frontend/src/components/SessionsPanel.tsx +0 -174
- data/frontend/src/components/ThreadsSidebar.tsx +0 -163
- data/lib/prompt_objects/server/public/assets/index-6y64NXFy.css +0 -1
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { useStore, useSelectedPO, usePONotifications } from '../store'
|
|
2
|
-
import { ChatPanel } from './ChatPanel'
|
|
3
|
-
import { SessionsPanel } from './SessionsPanel'
|
|
4
|
-
import { CapabilitiesPanel } from './CapabilitiesPanel'
|
|
5
|
-
import { PromptPanel } from './PromptPanel'
|
|
6
|
-
|
|
7
|
-
interface PODetailProps {
|
|
8
|
-
sendMessage: (target: string, content: string) => void
|
|
9
|
-
createSession: (target: string, name?: string) => void
|
|
10
|
-
switchSession: (target: string, sessionId: string) => void
|
|
11
|
-
createThread: (target: string, name?: string) => void
|
|
12
|
-
updatePrompt: (target: string, prompt: string) => void
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function PODetail({
|
|
16
|
-
sendMessage,
|
|
17
|
-
createSession,
|
|
18
|
-
switchSession,
|
|
19
|
-
createThread,
|
|
20
|
-
updatePrompt,
|
|
21
|
-
}: PODetailProps) {
|
|
22
|
-
const { activeTab, setActiveTab, selectPO } = useStore()
|
|
23
|
-
const po = useSelectedPO()
|
|
24
|
-
const notifications = usePONotifications(po?.name || '')
|
|
25
|
-
|
|
26
|
-
if (!po) {
|
|
27
|
-
return (
|
|
28
|
-
<div className="h-full flex items-center justify-center text-gray-500">
|
|
29
|
-
Select a Prompt Object
|
|
30
|
-
</div>
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const tabs = [
|
|
35
|
-
{ id: 'chat' as const, label: 'Chat' },
|
|
36
|
-
{ id: 'sessions' as const, label: `Threads (${po.sessions?.length || 0})` },
|
|
37
|
-
{ id: 'capabilities' as const, label: `Capabilities (${po.capabilities?.length || 0})` },
|
|
38
|
-
{ id: 'prompt' as const, label: 'Prompt' },
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div className="h-full flex flex-col">
|
|
43
|
-
{/* PO Header */}
|
|
44
|
-
<div className="border-b border-po-border bg-po-surface px-4 py-3">
|
|
45
|
-
<div className="flex items-center justify-between mb-2">
|
|
46
|
-
<div className="flex items-center gap-3">
|
|
47
|
-
<button
|
|
48
|
-
onClick={() => selectPO(null)}
|
|
49
|
-
className="text-gray-400 hover:text-white transition-colors"
|
|
50
|
-
>
|
|
51
|
-
← Back
|
|
52
|
-
</button>
|
|
53
|
-
<h2 className="text-lg font-semibold text-white">{po.name}</h2>
|
|
54
|
-
{notifications.length > 0 && (
|
|
55
|
-
<span className="bg-po-warning text-black text-xs font-bold px-2 py-0.5 rounded-full">
|
|
56
|
-
{notifications.length} pending
|
|
57
|
-
</span>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
<div className="flex items-center gap-2 text-sm">
|
|
61
|
-
<StatusIndicator status={po.status} />
|
|
62
|
-
</div>
|
|
63
|
-
</div>
|
|
64
|
-
<p className="text-sm text-gray-400">{po.description}</p>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
{/* Tabs */}
|
|
68
|
-
<div className="border-b border-po-border bg-po-surface px-4">
|
|
69
|
-
<div className="flex gap-1">
|
|
70
|
-
{tabs.map((tab) => (
|
|
71
|
-
<button
|
|
72
|
-
key={tab.id}
|
|
73
|
-
onClick={() => setActiveTab(tab.id)}
|
|
74
|
-
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
|
75
|
-
activeTab === tab.id
|
|
76
|
-
? 'text-po-accent border-po-accent'
|
|
77
|
-
: 'text-gray-400 border-transparent hover:text-white'
|
|
78
|
-
}`}
|
|
79
|
-
>
|
|
80
|
-
{tab.label}
|
|
81
|
-
</button>
|
|
82
|
-
))}
|
|
83
|
-
</div>
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
{/* Tab content */}
|
|
87
|
-
<div className="flex-1 overflow-hidden">
|
|
88
|
-
{activeTab === 'chat' && (
|
|
89
|
-
<ChatPanel po={po} sendMessage={sendMessage} />
|
|
90
|
-
)}
|
|
91
|
-
{activeTab === 'sessions' && (
|
|
92
|
-
<SessionsPanel
|
|
93
|
-
po={po}
|
|
94
|
-
createSession={createSession}
|
|
95
|
-
switchSession={switchSession}
|
|
96
|
-
createThread={createThread}
|
|
97
|
-
/>
|
|
98
|
-
)}
|
|
99
|
-
{activeTab === 'capabilities' && <CapabilitiesPanel po={po} />}
|
|
100
|
-
{activeTab === 'prompt' && (
|
|
101
|
-
<PromptPanel
|
|
102
|
-
po={po}
|
|
103
|
-
onSave={(prompt) => updatePrompt(po.name, prompt)}
|
|
104
|
-
/>
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
|
-
</div>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function StatusIndicator({ status }: { status: string }) {
|
|
112
|
-
const config = {
|
|
113
|
-
idle: { color: 'bg-gray-500', label: 'Idle' },
|
|
114
|
-
thinking: { color: 'bg-po-accent animate-pulse', label: 'Thinking...' },
|
|
115
|
-
calling_tool: { color: 'bg-po-warning animate-pulse', label: 'Calling tool...' },
|
|
116
|
-
}[status] || { color: 'bg-gray-500', label: status }
|
|
117
|
-
|
|
118
|
-
return (
|
|
119
|
-
<div className="flex items-center gap-2">
|
|
120
|
-
<div className={`w-2 h-2 rounded-full ${config.color}`} />
|
|
121
|
-
<span className="text-gray-400">{config.label}</span>
|
|
122
|
-
</div>
|
|
123
|
-
)
|
|
124
|
-
}
|
|
@@ -1,156 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
2
|
-
import type { PromptObject } from '../types'
|
|
3
|
-
import { MarkdownMessage } from './MarkdownMessage'
|
|
4
|
-
|
|
5
|
-
interface PromptPanelProps {
|
|
6
|
-
po: PromptObject
|
|
7
|
-
onSave?: (prompt: string) => void
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function PromptPanel({ po, onSave }: PromptPanelProps) {
|
|
11
|
-
const prompt = po.prompt || ''
|
|
12
|
-
const config = po.config || {}
|
|
13
|
-
const [isEditing, setIsEditing] = useState(false)
|
|
14
|
-
const [editedPrompt, setEditedPrompt] = useState(prompt)
|
|
15
|
-
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved')
|
|
16
|
-
const saveTimeoutRef = useRef<number | null>(null)
|
|
17
|
-
|
|
18
|
-
// Sync editedPrompt when po.prompt changes from server
|
|
19
|
-
useEffect(() => {
|
|
20
|
-
if (!isEditing) {
|
|
21
|
-
setEditedPrompt(prompt)
|
|
22
|
-
}
|
|
23
|
-
}, [prompt, isEditing])
|
|
24
|
-
|
|
25
|
-
// Debounced auto-save
|
|
26
|
-
const debouncedSave = useCallback((newPrompt: string) => {
|
|
27
|
-
if (saveTimeoutRef.current) {
|
|
28
|
-
clearTimeout(saveTimeoutRef.current)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
setSaveStatus('unsaved')
|
|
32
|
-
|
|
33
|
-
saveTimeoutRef.current = window.setTimeout(() => {
|
|
34
|
-
if (onSave && newPrompt !== prompt) {
|
|
35
|
-
setSaveStatus('saving')
|
|
36
|
-
onSave(newPrompt)
|
|
37
|
-
// Assume save succeeded - server will broadcast update
|
|
38
|
-
setTimeout(() => setSaveStatus('saved'), 500)
|
|
39
|
-
} else {
|
|
40
|
-
setSaveStatus('saved')
|
|
41
|
-
}
|
|
42
|
-
}, 1000) // 1 second debounce
|
|
43
|
-
}, [onSave, prompt])
|
|
44
|
-
|
|
45
|
-
const handlePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
46
|
-
const newPrompt = e.target.value
|
|
47
|
-
setEditedPrompt(newPrompt)
|
|
48
|
-
debouncedSave(newPrompt)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const handleStartEditing = () => {
|
|
52
|
-
setEditedPrompt(prompt)
|
|
53
|
-
setIsEditing(true)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const handleStopEditing = () => {
|
|
57
|
-
// Save any pending changes
|
|
58
|
-
if (saveTimeoutRef.current) {
|
|
59
|
-
clearTimeout(saveTimeoutRef.current)
|
|
60
|
-
}
|
|
61
|
-
if (editedPrompt !== prompt && onSave) {
|
|
62
|
-
onSave(editedPrompt)
|
|
63
|
-
}
|
|
64
|
-
setIsEditing(false)
|
|
65
|
-
setSaveStatus('saved')
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Cleanup timeout on unmount
|
|
69
|
-
useEffect(() => {
|
|
70
|
-
return () => {
|
|
71
|
-
if (saveTimeoutRef.current) {
|
|
72
|
-
clearTimeout(saveTimeoutRef.current)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
}, [])
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div className="h-full overflow-auto p-4">
|
|
79
|
-
{/* Config/Frontmatter */}
|
|
80
|
-
<div className="mb-6">
|
|
81
|
-
<h3 className="text-lg font-medium text-white mb-3">Configuration</h3>
|
|
82
|
-
<div className="bg-po-bg rounded-lg border border-po-border p-4">
|
|
83
|
-
<pre className="text-sm text-gray-300 font-mono whitespace-pre-wrap">
|
|
84
|
-
{JSON.stringify(config, null, 2)}
|
|
85
|
-
</pre>
|
|
86
|
-
</div>
|
|
87
|
-
</div>
|
|
88
|
-
|
|
89
|
-
{/* Prompt/Body */}
|
|
90
|
-
<div>
|
|
91
|
-
<div className="flex items-center justify-between mb-3">
|
|
92
|
-
<h3 className="text-lg font-medium text-white">Prompt</h3>
|
|
93
|
-
<div className="flex items-center gap-3">
|
|
94
|
-
{isEditing && (
|
|
95
|
-
<span className={`text-xs ${
|
|
96
|
-
saveStatus === 'saved' ? 'text-green-400' :
|
|
97
|
-
saveStatus === 'saving' ? 'text-yellow-400' :
|
|
98
|
-
'text-gray-400'
|
|
99
|
-
}`}>
|
|
100
|
-
{saveStatus === 'saved' ? 'Saved' :
|
|
101
|
-
saveStatus === 'saving' ? 'Saving...' :
|
|
102
|
-
'Unsaved changes'}
|
|
103
|
-
</span>
|
|
104
|
-
)}
|
|
105
|
-
<button
|
|
106
|
-
onClick={isEditing ? handleStopEditing : handleStartEditing}
|
|
107
|
-
className={`px-3 py-1 text-sm rounded transition-colors ${
|
|
108
|
-
isEditing
|
|
109
|
-
? 'bg-po-accent text-black hover:bg-po-accent/80'
|
|
110
|
-
: 'bg-po-surface border border-po-border text-gray-300 hover:text-white hover:border-po-accent'
|
|
111
|
-
}`}
|
|
112
|
-
>
|
|
113
|
-
{isEditing ? 'Done' : 'Edit'}
|
|
114
|
-
</button>
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
117
|
-
|
|
118
|
-
<div className="bg-po-bg rounded-lg border border-po-border">
|
|
119
|
-
{isEditing ? (
|
|
120
|
-
<textarea
|
|
121
|
-
value={editedPrompt}
|
|
122
|
-
onChange={handlePromptChange}
|
|
123
|
-
className="w-full h-96 p-4 bg-transparent text-gray-200 font-mono text-sm resize-none focus:outline-none focus:ring-1 focus:ring-po-accent rounded-lg"
|
|
124
|
-
placeholder="Enter prompt markdown..."
|
|
125
|
-
spellCheck={false}
|
|
126
|
-
/>
|
|
127
|
-
) : (
|
|
128
|
-
<div className="p-4">
|
|
129
|
-
{prompt ? (
|
|
130
|
-
<div className="prose prose-invert max-w-none">
|
|
131
|
-
<MarkdownMessage content={prompt} />
|
|
132
|
-
</div>
|
|
133
|
-
) : (
|
|
134
|
-
<p className="text-gray-500 italic">No prompt defined. Click Edit to add one.</p>
|
|
135
|
-
)}
|
|
136
|
-
</div>
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
{/* Raw source toggle (only in view mode) */}
|
|
142
|
-
{!isEditing && (
|
|
143
|
-
<details className="mt-6">
|
|
144
|
-
<summary className="text-sm text-gray-400 cursor-pointer hover:text-white">
|
|
145
|
-
View raw source
|
|
146
|
-
</summary>
|
|
147
|
-
<div className="mt-2 bg-po-bg rounded-lg border border-po-border p-4">
|
|
148
|
-
<pre className="text-xs text-gray-400 font-mono whitespace-pre-wrap overflow-x-auto">
|
|
149
|
-
{prompt || '(empty)'}
|
|
150
|
-
</pre>
|
|
151
|
-
</div>
|
|
152
|
-
</details>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
155
|
-
)
|
|
156
|
-
}
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { useMemo } from 'react'
|
|
2
|
-
import type { PromptObject, Session, ThreadType } from '../types'
|
|
3
|
-
|
|
4
|
-
interface SessionsPanelProps {
|
|
5
|
-
po: PromptObject
|
|
6
|
-
createSession: (target: string, name?: string) => void
|
|
7
|
-
switchSession: (target: string, sessionId: string) => void
|
|
8
|
-
createThread?: (target: string, name?: string) => void
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
// Build a tree structure from flat sessions list
|
|
12
|
-
interface ThreadNode {
|
|
13
|
-
session: Session
|
|
14
|
-
children: ThreadNode[]
|
|
15
|
-
depth: number
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function buildThreadTree(sessions: Session[]): ThreadNode[] {
|
|
19
|
-
const sessionMap = new Map<string, Session>()
|
|
20
|
-
const childrenMap = new Map<string, Session[]>()
|
|
21
|
-
|
|
22
|
-
// Index all sessions
|
|
23
|
-
sessions.forEach((s) => {
|
|
24
|
-
sessionMap.set(s.id, s)
|
|
25
|
-
if (s.parent_session_id) {
|
|
26
|
-
const children = childrenMap.get(s.parent_session_id) || []
|
|
27
|
-
children.push(s)
|
|
28
|
-
childrenMap.set(s.parent_session_id, children)
|
|
29
|
-
}
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
// Build tree recursively
|
|
33
|
-
function buildNode(session: Session, depth: number): ThreadNode {
|
|
34
|
-
const children = childrenMap.get(session.id) || []
|
|
35
|
-
return {
|
|
36
|
-
session,
|
|
37
|
-
children: children
|
|
38
|
-
.sort((a, b) => (a.updated_at || '').localeCompare(b.updated_at || ''))
|
|
39
|
-
.map((child) => buildNode(child, depth + 1)),
|
|
40
|
-
depth,
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
// Get root sessions (no parent)
|
|
45
|
-
const roots = sessions
|
|
46
|
-
.filter((s) => !s.parent_session_id)
|
|
47
|
-
.sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''))
|
|
48
|
-
|
|
49
|
-
return roots.map((root) => buildNode(root, 0))
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Flatten tree for rendering
|
|
53
|
-
function flattenTree(nodes: ThreadNode[]): ThreadNode[] {
|
|
54
|
-
const result: ThreadNode[] = []
|
|
55
|
-
function traverse(node: ThreadNode) {
|
|
56
|
-
result.push(node)
|
|
57
|
-
node.children.forEach(traverse)
|
|
58
|
-
}
|
|
59
|
-
nodes.forEach(traverse)
|
|
60
|
-
return result
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Thread type icons/badges
|
|
64
|
-
function ThreadTypeBadge({ type }: { type: ThreadType }) {
|
|
65
|
-
switch (type) {
|
|
66
|
-
case 'delegation':
|
|
67
|
-
return (
|
|
68
|
-
<span className="text-xs bg-purple-600/30 text-purple-300 px-1.5 py-0.5 rounded">
|
|
69
|
-
↳ delegation
|
|
70
|
-
</span>
|
|
71
|
-
)
|
|
72
|
-
case 'fork':
|
|
73
|
-
return (
|
|
74
|
-
<span className="text-xs bg-blue-600/30 text-blue-300 px-1.5 py-0.5 rounded">
|
|
75
|
-
⑂ fork
|
|
76
|
-
</span>
|
|
77
|
-
)
|
|
78
|
-
case 'continuation':
|
|
79
|
-
return (
|
|
80
|
-
<span className="text-xs bg-gray-600/30 text-gray-300 px-1.5 py-0.5 rounded">
|
|
81
|
-
→ continued
|
|
82
|
-
</span>
|
|
83
|
-
)
|
|
84
|
-
default:
|
|
85
|
-
return null
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function SessionsPanel({
|
|
90
|
-
po,
|
|
91
|
-
createSession,
|
|
92
|
-
switchSession,
|
|
93
|
-
createThread,
|
|
94
|
-
}: SessionsPanelProps) {
|
|
95
|
-
const sessions = po.sessions || []
|
|
96
|
-
const currentSessionId = po.current_session?.id
|
|
97
|
-
|
|
98
|
-
// Build and flatten thread tree
|
|
99
|
-
const flatNodes = useMemo(() => {
|
|
100
|
-
const tree = buildThreadTree(sessions)
|
|
101
|
-
return flattenTree(tree)
|
|
102
|
-
}, [sessions])
|
|
103
|
-
|
|
104
|
-
const handleNewThread = () => {
|
|
105
|
-
if (createThread) {
|
|
106
|
-
createThread(po.name)
|
|
107
|
-
} else {
|
|
108
|
-
// Fallback to createSession
|
|
109
|
-
const name = prompt('Thread name (optional):')
|
|
110
|
-
createSession(po.name, name || undefined)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return (
|
|
115
|
-
<div className="h-full overflow-auto p-4">
|
|
116
|
-
<div className="flex items-center justify-between mb-4">
|
|
117
|
-
<h3 className="text-lg font-medium text-white">Threads</h3>
|
|
118
|
-
<button
|
|
119
|
-
onClick={handleNewThread}
|
|
120
|
-
className="px-3 py-1.5 bg-po-accent text-white text-sm rounded hover:bg-po-accent/80 transition-colors"
|
|
121
|
-
>
|
|
122
|
-
+ New Thread
|
|
123
|
-
</button>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
{flatNodes.length === 0 ? (
|
|
127
|
-
<div className="text-gray-500 text-center py-8">
|
|
128
|
-
No threads yet. Start a conversation to create one.
|
|
129
|
-
</div>
|
|
130
|
-
) : (
|
|
131
|
-
<div className="space-y-1">
|
|
132
|
-
{flatNodes.map(({ session, depth }) => (
|
|
133
|
-
<button
|
|
134
|
-
key={session.id}
|
|
135
|
-
onClick={() => switchSession(po.name, session.id)}
|
|
136
|
-
className={`w-full text-left p-3 rounded-lg border transition-colors ${
|
|
137
|
-
session.id === currentSessionId
|
|
138
|
-
? 'bg-po-accent/20 border-po-accent'
|
|
139
|
-
: 'bg-po-surface border-po-border hover:border-po-accent/50'
|
|
140
|
-
}`}
|
|
141
|
-
style={{ marginLeft: `${depth * 16}px`, width: `calc(100% - ${depth * 16}px)` }}
|
|
142
|
-
>
|
|
143
|
-
<div className="flex items-center gap-2 mb-1">
|
|
144
|
-
{depth > 0 && (
|
|
145
|
-
<span className="text-gray-500 text-xs">↳</span>
|
|
146
|
-
)}
|
|
147
|
-
<span className="font-medium text-white truncate flex-1">
|
|
148
|
-
{session.name || `Thread ${session.id.slice(0, 8)}`}
|
|
149
|
-
</span>
|
|
150
|
-
<ThreadTypeBadge type={session.thread_type} />
|
|
151
|
-
{session.id === currentSessionId && (
|
|
152
|
-
<span className="text-xs bg-po-accent text-white px-2 py-0.5 rounded flex-shrink-0">
|
|
153
|
-
Active
|
|
154
|
-
</span>
|
|
155
|
-
)}
|
|
156
|
-
</div>
|
|
157
|
-
<div className="text-sm text-gray-400 flex items-center gap-2">
|
|
158
|
-
<span>{session.message_count} messages</span>
|
|
159
|
-
{session.parent_po && (
|
|
160
|
-
<span className="text-purple-400">from {session.parent_po}</span>
|
|
161
|
-
)}
|
|
162
|
-
{session.updated_at && (
|
|
163
|
-
<span className="ml-auto">
|
|
164
|
-
{new Date(session.updated_at).toLocaleDateString()}
|
|
165
|
-
</span>
|
|
166
|
-
)}
|
|
167
|
-
</div>
|
|
168
|
-
</button>
|
|
169
|
-
))}
|
|
170
|
-
</div>
|
|
171
|
-
)}
|
|
172
|
-
</div>
|
|
173
|
-
)
|
|
174
|
-
}
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react'
|
|
2
|
-
import { ContextMenu } from './ContextMenu'
|
|
3
|
-
import type { PromptObject, Session, ThreadType } from '../types'
|
|
4
|
-
|
|
5
|
-
interface ThreadsSidebarProps {
|
|
6
|
-
po: PromptObject
|
|
7
|
-
switchSession: (target: string, sessionId: string) => void
|
|
8
|
-
createThread: (target: string) => void
|
|
9
|
-
requestUsage?: (sessionId: string, includeTree?: boolean) => void
|
|
10
|
-
exportThread?: (sessionId: string, format?: string) => void
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Build a flat list with depth info
|
|
14
|
-
interface ThreadItem {
|
|
15
|
-
session: Session
|
|
16
|
-
depth: number
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function buildThreadList(sessions: Session[]): ThreadItem[] {
|
|
20
|
-
const childrenMap = new Map<string, Session[]>()
|
|
21
|
-
|
|
22
|
-
// Index children
|
|
23
|
-
sessions.forEach((s) => {
|
|
24
|
-
if (s.parent_session_id) {
|
|
25
|
-
const children = childrenMap.get(s.parent_session_id) || []
|
|
26
|
-
children.push(s)
|
|
27
|
-
childrenMap.set(s.parent_session_id, children)
|
|
28
|
-
}
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
// Build flat list with depth
|
|
32
|
-
const result: ThreadItem[] = []
|
|
33
|
-
|
|
34
|
-
function traverse(session: Session, depth: number) {
|
|
35
|
-
result.push({ session, depth })
|
|
36
|
-
const children = childrenMap.get(session.id) || []
|
|
37
|
-
children
|
|
38
|
-
.sort((a, b) => (a.updated_at || '').localeCompare(b.updated_at || ''))
|
|
39
|
-
.forEach((child) => traverse(child, depth + 1))
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Get roots and traverse
|
|
43
|
-
const roots = sessions
|
|
44
|
-
.filter((s) => !s.parent_session_id)
|
|
45
|
-
.sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''))
|
|
46
|
-
|
|
47
|
-
roots.forEach((root) => traverse(root, 0))
|
|
48
|
-
|
|
49
|
-
return result
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function ThreadTypeIcon({ type }: { type: ThreadType }) {
|
|
53
|
-
switch (type) {
|
|
54
|
-
case 'delegation':
|
|
55
|
-
return <span className="text-purple-400" title="Delegation">↳</span>
|
|
56
|
-
case 'fork':
|
|
57
|
-
return <span className="text-blue-400" title="Fork">⑂</span>
|
|
58
|
-
case 'continuation':
|
|
59
|
-
return <span className="text-gray-400" title="Continuation">→</span>
|
|
60
|
-
default:
|
|
61
|
-
return null
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function ThreadsSidebar({ po, switchSession, createThread, requestUsage, exportThread }: ThreadsSidebarProps) {
|
|
66
|
-
const sessions = po.sessions || []
|
|
67
|
-
const currentSessionId = po.current_session?.id
|
|
68
|
-
const [contextMenu, setContextMenu] = useState<{ x: number; y: number; sessionId: string } | null>(null)
|
|
69
|
-
|
|
70
|
-
const threadList = useMemo(() => buildThreadList(sessions), [sessions])
|
|
71
|
-
|
|
72
|
-
const handleContextMenu = (e: React.MouseEvent, sessionId: string) => {
|
|
73
|
-
e.preventDefault()
|
|
74
|
-
setContextMenu({ x: e.clientX, y: e.clientY, sessionId })
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return (
|
|
78
|
-
<div className="h-full flex flex-col">
|
|
79
|
-
<div className="p-3 border-b border-po-border flex items-center justify-between">
|
|
80
|
-
<h2 className="text-sm font-medium text-gray-400">Threads</h2>
|
|
81
|
-
<button
|
|
82
|
-
onClick={() => createThread(po.name)}
|
|
83
|
-
className="text-xs text-gray-500 hover:text-po-accent transition-colors"
|
|
84
|
-
title="New thread"
|
|
85
|
-
>
|
|
86
|
-
+ New
|
|
87
|
-
</button>
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
<div className="flex-1 overflow-auto p-2 space-y-1">
|
|
91
|
-
{threadList.length === 0 ? (
|
|
92
|
-
<div className="text-xs text-gray-500 text-center py-4">
|
|
93
|
-
No threads yet
|
|
94
|
-
</div>
|
|
95
|
-
) : (
|
|
96
|
-
threadList.map(({ session, depth }) => (
|
|
97
|
-
<button
|
|
98
|
-
key={session.id}
|
|
99
|
-
onClick={() => switchSession(po.name, session.id)}
|
|
100
|
-
onContextMenu={(e) => handleContextMenu(e, session.id)}
|
|
101
|
-
className={`w-full text-left p-2 rounded text-xs transition-colors ${
|
|
102
|
-
session.id === currentSessionId
|
|
103
|
-
? 'bg-po-accent/20 border border-po-accent'
|
|
104
|
-
: 'hover:bg-po-surface border border-transparent'
|
|
105
|
-
}`}
|
|
106
|
-
style={{ paddingLeft: `${8 + depth * 12}px` }}
|
|
107
|
-
>
|
|
108
|
-
<div className="flex items-center gap-1">
|
|
109
|
-
{depth > 0 && <ThreadTypeIcon type={session.thread_type} />}
|
|
110
|
-
<span className={`truncate flex-1 ${session.id === currentSessionId ? 'text-white' : 'text-gray-300'}`}>
|
|
111
|
-
{session.name || `Thread ${session.id.slice(0, 6)}`}
|
|
112
|
-
</span>
|
|
113
|
-
{session.id === currentSessionId && (
|
|
114
|
-
<span className="w-1.5 h-1.5 rounded-full bg-po-accent flex-shrink-0" />
|
|
115
|
-
)}
|
|
116
|
-
</div>
|
|
117
|
-
<div className="text-gray-500 text-[10px] mt-0.5">
|
|
118
|
-
{session.message_count} msgs
|
|
119
|
-
{session.parent_po && (
|
|
120
|
-
<span className="text-purple-400 ml-1">from {session.parent_po}</span>
|
|
121
|
-
)}
|
|
122
|
-
</div>
|
|
123
|
-
</button>
|
|
124
|
-
))
|
|
125
|
-
)}
|
|
126
|
-
</div>
|
|
127
|
-
|
|
128
|
-
{contextMenu && (
|
|
129
|
-
<ContextMenu
|
|
130
|
-
x={contextMenu.x}
|
|
131
|
-
y={contextMenu.y}
|
|
132
|
-
onClose={() => setContextMenu(null)}
|
|
133
|
-
items={[
|
|
134
|
-
...(requestUsage ? [
|
|
135
|
-
{
|
|
136
|
-
label: 'View Usage',
|
|
137
|
-
icon: '📊',
|
|
138
|
-
onClick: () => requestUsage(contextMenu.sessionId),
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
label: 'View Tree Usage',
|
|
142
|
-
icon: '🌳',
|
|
143
|
-
onClick: () => requestUsage(contextMenu.sessionId, true),
|
|
144
|
-
},
|
|
145
|
-
] : []),
|
|
146
|
-
...(exportThread ? [
|
|
147
|
-
{
|
|
148
|
-
label: 'Export as Markdown',
|
|
149
|
-
icon: '📄',
|
|
150
|
-
onClick: () => exportThread(contextMenu.sessionId, 'markdown'),
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
label: 'Export as JSON',
|
|
154
|
-
icon: '📋',
|
|
155
|
-
onClick: () => exportThread(contextMenu.sessionId, 'json'),
|
|
156
|
-
},
|
|
157
|
-
] : []),
|
|
158
|
-
]}
|
|
159
|
-
/>
|
|
160
|
-
)}
|
|
161
|
-
</div>
|
|
162
|
-
)
|
|
163
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.\!container{width:100%!important}.container{width:100%}@media(min-width:640px){.\!container{max-width:640px!important}.container{max-width:640px}}@media(min-width:768px){.\!container{max-width:768px!important}.container{max-width:768px}}@media(min-width:1024px){.\!container{max-width:1024px!important}.container{max-width:1024px}}@media(min-width:1280px){.\!container{max-width:1280px!important}.container{max-width:1280px}}@media(min-width:1536px){.\!container{max-width:1536px!important}.container{max-width:1536px}}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-3{bottom:.75rem}.bottom-4{bottom:1rem}.left-2{left:.5rem}.left-3{left:.75rem}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-16{top:4rem}.top-3{top:.75rem}.top-full{top:100%}.z-10{z-index:10}.z-50{z-index:50}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-11{margin-left:2.75rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-4{height:1rem}.h-40{height:10rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-96{height:24rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-32{max-height:8rem}.max-h-48{max-height:12rem}.max-h-64{max-height:16rem}.max-h-\[60vh\]{max-height:60vh}.max-h-\[80vh\]{max-height:80vh}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-96{width:24rem}.w-\[420px\]{width:420px}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-full{min-width:100%}.max-w-\[80\%\]{max-width:80%}.max-w-none{max-width:none}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.resize{resize:both}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-0{border-bottom-width:0px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-po-accent{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.border-po-border{--tw-border-opacity: 1;border-color:rgb(45 45 68 / var(--tw-border-opacity, 1))}.border-po-border\/30{border-color:#2d2d444d}.border-po-border\/50{border-color:#2d2d4480}.border-po-warning\/30{border-color:#f59e0b4d}.border-transparent{border-color:transparent}.bg-black\/50{background-color:#00000080}.bg-blue-600\/30{background-color:#2563eb4d}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-600\/30{background-color:#4b55634d}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-po-accent{--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity, 1))}.bg-po-accent\/20{background-color:#7c3aed33}.bg-po-bg{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}.bg-po-bg\/50{background-color:#0f0f1a80}.bg-po-bg\/80{background-color:#0f0f1acc}.bg-po-border{--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}.bg-po-surface{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.bg-po-surface\/50{background-color:#1a1a2e80}.bg-po-surface\/80{background-color:#1a1a2ecc}.bg-po-warning{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-po-warning\/10{background-color:#f59e0b1a}.bg-purple-600\/30{background-color:#9333ea4d}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-po-accent{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.text-po-accent\/70{color:#7c3aedb3}.text-po-success{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-po-warning{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.ring{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.backdrop-blur{--tw-backdrop-blur: blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}html{color-scheme:dark}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity, 1))}.canvas-node-label{text-align:center;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none}.canvas-node-name{display:block;color:#fff;font-size:12px;font-weight:500;text-shadow:0 1px 4px rgba(0,0,0,.8)}.canvas-node-status{display:block;color:#6b7280;font-size:10px;text-shadow:0 1px 4px rgba(0,0,0,.8)}.canvas-node-badge{display:flex;align-items:center;justify-content:center;width:20px;height:20px;background:#f59e0b;color:#000;font-size:10px;font-weight:700;border-radius:50%;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;box-shadow:0 0 8px #f59e0b99}.canvas-toolcall-label{color:#93c5fd;font-size:10px;font-family:monospace;text-align:center;pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;text-shadow:0 1px 4px rgba(0,0,0,.8)}.first\:mt-0:first-child{margin-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-0:last-child{border-width:0px}.hover\:border-po-accent:hover{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.hover\:border-po-accent\/50:hover{border-color:#7c3aed80}.hover\:bg-po-accent\/50:hover{background-color:#7c3aed80}.hover\:bg-po-accent\/80:hover{background-color:#7c3aedcc}.hover\:bg-po-bg:hover{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}.hover\:bg-po-bg\/70:hover{background-color:#0f0f1ab3}.hover\:bg-po-border:hover{--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}.hover\:bg-po-surface:hover{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.hover\:bg-po-warning\/20:hover{background-color:#f59e0b33}.hover\:text-po-accent:hover{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-po-accent:focus{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-po-accent:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(124 58 237 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-po-accent{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}
|