prompt_objects 0.3.1 → 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 +32 -0
- data/CLAUDE.md +112 -44
- data/Gemfile.lock +31 -29
- data/README.md +5 -0
- data/frontend/index.html +5 -1
- data/frontend/package-lock.json +123 -0
- data/frontend/package.json +4 -0
- data/frontend/src/App.tsx +70 -71
- data/frontend/src/canvas/CanvasView.tsx +113 -0
- data/frontend/src/canvas/ForceLayout.ts +115 -0
- data/frontend/src/canvas/SceneManager.ts +587 -0
- data/frontend/src/canvas/canvasStore.ts +47 -0
- data/frontend/src/canvas/constants.ts +95 -0
- data/frontend/src/canvas/controls/CameraControls.ts +98 -0
- data/frontend/src/canvas/edges/MessageArc.ts +149 -0
- data/frontend/src/canvas/inspector/InspectorPanel.tsx +31 -0
- data/frontend/src/canvas/inspector/POInspector.tsx +262 -0
- data/frontend/src/canvas/inspector/ToolCallInspector.tsx +67 -0
- data/frontend/src/canvas/nodes/PONode.ts +249 -0
- data/frontend/src/canvas/nodes/ToolCallNode.ts +116 -0
- data/frontend/src/canvas/types.ts +24 -0
- 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 +274 -189
- data/frontend/src/index.css +69 -3
- data/frontend/src/store/index.ts +23 -0
- data/frontend/src/types/index.ts +5 -0
- data/frontend/tailwind.config.js +28 -9
- data/lib/prompt_objects/capability.rb +23 -1
- data/lib/prompt_objects/connectors/mcp.rb +5 -22
- data/lib/prompt_objects/environment.rb +8 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +1 -2
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +2 -8
- data/lib/prompt_objects/primitives/list_files.rb +1 -2
- data/lib/prompt_objects/prompt_object.rb +150 -6
- data/lib/prompt_objects/server/api/routes.rb +3 -48
- data/lib/prompt_objects/server/app.rb +9 -0
- data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +4345 -0
- 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 +26 -14
- data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
- data/frontend/src/components/ChatPanel.tsx +0 -288
- data/frontend/src/components/Dashboard.tsx +0 -83
- data/frontend/src/components/Header.tsx +0 -141
- 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-Bkme6COu.css +0 -1
- data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +0 -77
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { useRef, useEffect } from 'react'
|
|
2
|
-
import { useStore } from '../store'
|
|
3
|
-
|
|
4
|
-
export function MessageBus() {
|
|
5
|
-
const { busMessages, toggleBus } = useStore()
|
|
6
|
-
const messagesEndRef = useRef<HTMLDivElement>(null)
|
|
7
|
-
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
10
|
-
}, [busMessages])
|
|
11
|
-
|
|
12
|
-
return (
|
|
13
|
-
<div className="h-full flex flex-col">
|
|
14
|
-
<div className="flex items-center justify-between p-3 border-b border-po-border">
|
|
15
|
-
<h3 className="font-medium text-white">Message Bus</h3>
|
|
16
|
-
<button
|
|
17
|
-
onClick={toggleBus}
|
|
18
|
-
className="text-gray-400 hover:text-white transition-colors"
|
|
19
|
-
>
|
|
20
|
-
✕
|
|
21
|
-
</button>
|
|
22
|
-
</div>
|
|
23
|
-
|
|
24
|
-
<div className="flex-1 overflow-auto p-3 space-y-2">
|
|
25
|
-
{busMessages.length === 0 ? (
|
|
26
|
-
<div className="text-gray-500 text-sm text-center py-4">
|
|
27
|
-
No messages yet
|
|
28
|
-
</div>
|
|
29
|
-
) : (
|
|
30
|
-
busMessages.map((msg, index) => (
|
|
31
|
-
<div
|
|
32
|
-
key={index}
|
|
33
|
-
className="text-xs bg-po-bg rounded p-2 border border-po-border"
|
|
34
|
-
>
|
|
35
|
-
<div className="flex items-center gap-2 mb-1">
|
|
36
|
-
<span className="text-po-accent font-medium">{msg.from}</span>
|
|
37
|
-
<span className="text-gray-500">→</span>
|
|
38
|
-
<span className="text-po-warning font-medium">{msg.to}</span>
|
|
39
|
-
<span className="text-gray-600 ml-auto">
|
|
40
|
-
{new Date(msg.timestamp).toLocaleTimeString()}
|
|
41
|
-
</span>
|
|
42
|
-
</div>
|
|
43
|
-
<div className="text-gray-300 break-words">
|
|
44
|
-
{(() => {
|
|
45
|
-
const text = msg.summary || (typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content))
|
|
46
|
-
return text.length > 200 ? text.slice(0, 200) + '...' : text
|
|
47
|
-
})()}
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
))
|
|
51
|
-
)}
|
|
52
|
-
<div ref={messagesEndRef} />
|
|
53
|
-
</div>
|
|
54
|
-
</div>
|
|
55
|
-
)
|
|
56
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { useStore, usePONotifications } from '../store'
|
|
2
|
-
import type { PromptObject } from '../types'
|
|
3
|
-
|
|
4
|
-
interface POCardProps {
|
|
5
|
-
po: PromptObject
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function POCard({ po }: POCardProps) {
|
|
9
|
-
const { selectPO } = useStore()
|
|
10
|
-
const notifications = usePONotifications(po.name)
|
|
11
|
-
|
|
12
|
-
const statusColors = {
|
|
13
|
-
idle: 'bg-gray-500',
|
|
14
|
-
thinking: 'bg-po-accent animate-pulse',
|
|
15
|
-
calling_tool: 'bg-po-warning animate-pulse',
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const statusLabels = {
|
|
19
|
-
idle: 'Idle',
|
|
20
|
-
thinking: 'Thinking...',
|
|
21
|
-
calling_tool: 'Calling tool...',
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return (
|
|
25
|
-
<button
|
|
26
|
-
onClick={() => selectPO(po.name)}
|
|
27
|
-
className="bg-po-surface border border-po-border rounded-lg p-4 text-left hover:border-po-accent transition-colors group"
|
|
28
|
-
>
|
|
29
|
-
<div className="flex items-start justify-between mb-2">
|
|
30
|
-
<h3 className="font-medium text-white group-hover:text-po-accent transition-colors">
|
|
31
|
-
{po.name}
|
|
32
|
-
</h3>
|
|
33
|
-
<div className="flex items-center gap-2">
|
|
34
|
-
{notifications.length > 0 && (
|
|
35
|
-
<span className="bg-po-warning text-black text-xs font-bold px-1.5 py-0.5 rounded-full">
|
|
36
|
-
{notifications.length}
|
|
37
|
-
</span>
|
|
38
|
-
)}
|
|
39
|
-
<div
|
|
40
|
-
className={`w-2 h-2 rounded-full ${statusColors[po.status]}`}
|
|
41
|
-
title={statusLabels[po.status]}
|
|
42
|
-
/>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
|
|
46
|
-
<p className="text-sm text-gray-400 line-clamp-2 mb-3">
|
|
47
|
-
{po.description || 'No description'}
|
|
48
|
-
</p>
|
|
49
|
-
|
|
50
|
-
<div className="flex items-center justify-between text-xs text-gray-500">
|
|
51
|
-
<span>{po.capabilities?.length || 0} capabilities</span>
|
|
52
|
-
<span>{po.sessions?.length || 0} sessions</span>
|
|
53
|
-
</div>
|
|
54
|
-
</button>
|
|
55
|
-
)
|
|
56
|
-
}
|
|
@@ -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
|
-
}
|