prompt_objects 0.1.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 +7 -0
- data/CLAUDE.md +108 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +231 -0
- data/IMPLEMENTATION_PLAN.md +1073 -0
- data/LICENSE +21 -0
- data/README.md +73 -0
- data/Rakefile +27 -0
- data/design-doc-v2.md +1232 -0
- data/exe/prompt_objects +572 -0
- data/exe/prompt_objects_mcp +34 -0
- data/frontend/.gitignore +3 -0
- data/frontend/index.html +13 -0
- data/frontend/package-lock.json +4417 -0
- data/frontend/package.json +32 -0
- data/frontend/postcss.config.js +6 -0
- data/frontend/src/App.tsx +95 -0
- data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
- data/frontend/src/components/ChatPanel.tsx +251 -0
- data/frontend/src/components/Dashboard.tsx +83 -0
- data/frontend/src/components/Header.tsx +141 -0
- data/frontend/src/components/MarkdownMessage.tsx +153 -0
- data/frontend/src/components/MessageBus.tsx +55 -0
- data/frontend/src/components/ModelSelector.tsx +112 -0
- data/frontend/src/components/NotificationPanel.tsx +134 -0
- data/frontend/src/components/POCard.tsx +56 -0
- data/frontend/src/components/PODetail.tsx +117 -0
- data/frontend/src/components/PromptPanel.tsx +51 -0
- data/frontend/src/components/SessionsPanel.tsx +174 -0
- data/frontend/src/components/ThreadsSidebar.tsx +119 -0
- data/frontend/src/components/index.ts +11 -0
- data/frontend/src/hooks/useWebSocket.ts +363 -0
- data/frontend/src/index.css +37 -0
- data/frontend/src/main.tsx +10 -0
- data/frontend/src/store/index.ts +246 -0
- data/frontend/src/types/index.ts +146 -0
- data/frontend/tailwind.config.js +25 -0
- data/frontend/tsconfig.json +30 -0
- data/frontend/vite.config.ts +29 -0
- data/lib/prompt_objects/capability.rb +46 -0
- data/lib/prompt_objects/cli.rb +431 -0
- data/lib/prompt_objects/connectors/base.rb +73 -0
- data/lib/prompt_objects/connectors/mcp.rb +524 -0
- data/lib/prompt_objects/environment/exporter.rb +83 -0
- data/lib/prompt_objects/environment/git.rb +118 -0
- data/lib/prompt_objects/environment/importer.rb +159 -0
- data/lib/prompt_objects/environment/manager.rb +401 -0
- data/lib/prompt_objects/environment/manifest.rb +218 -0
- data/lib/prompt_objects/environment.rb +283 -0
- data/lib/prompt_objects/human_queue.rb +144 -0
- data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
- data/lib/prompt_objects/llm/factory.rb +84 -0
- data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
- data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
- data/lib/prompt_objects/llm/response.rb +61 -0
- data/lib/prompt_objects/loader.rb +32 -0
- data/lib/prompt_objects/mcp/server.rb +167 -0
- data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
- data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
- data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
- data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
- data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
- data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
- data/lib/prompt_objects/message_bus.rb +97 -0
- data/lib/prompt_objects/primitive.rb +13 -0
- data/lib/prompt_objects/primitives/http_get.rb +72 -0
- data/lib/prompt_objects/primitives/list_files.rb +95 -0
- data/lib/prompt_objects/primitives/read_file.rb +81 -0
- data/lib/prompt_objects/primitives/write_file.rb +73 -0
- data/lib/prompt_objects/prompt_object.rb +415 -0
- data/lib/prompt_objects/registry.rb +88 -0
- data/lib/prompt_objects/server/api/routes.rb +297 -0
- data/lib/prompt_objects/server/app.rb +174 -0
- data/lib/prompt_objects/server/file_watcher.rb +113 -0
- data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
- data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
- data/lib/prompt_objects/server/public/index.html +14 -0
- data/lib/prompt_objects/server/websocket_handler.rb +619 -0
- data/lib/prompt_objects/server.rb +166 -0
- data/lib/prompt_objects/session/store.rb +826 -0
- data/lib/prompt_objects/universal/add_capability.rb +74 -0
- data/lib/prompt_objects/universal/add_primitive.rb +113 -0
- data/lib/prompt_objects/universal/ask_human.rb +109 -0
- data/lib/prompt_objects/universal/create_capability.rb +219 -0
- data/lib/prompt_objects/universal/create_primitive.rb +170 -0
- data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
- data/lib/prompt_objects/universal/list_primitives.rb +145 -0
- data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
- data/lib/prompt_objects/universal/request_primitive.rb +287 -0
- data/lib/prompt_objects/universal/think.rb +41 -0
- data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
- data/lib/prompt_objects.rb +62 -0
- data/objects/coordinator.md +48 -0
- data/objects/greeter.md +30 -0
- data/objects/reader.md +33 -0
- data/prompt_objects.gemspec +50 -0
- data/templates/basic/.gitignore +2 -0
- data/templates/basic/manifest.yml +7 -0
- data/templates/basic/objects/basic.md +32 -0
- data/templates/developer/.gitignore +5 -0
- data/templates/developer/manifest.yml +17 -0
- data/templates/developer/objects/code_reviewer.md +33 -0
- data/templates/developer/objects/coordinator.md +39 -0
- data/templates/developer/objects/debugger.md +35 -0
- data/templates/empty/.gitignore +5 -0
- data/templates/empty/manifest.yml +14 -0
- data/templates/empty/objects/.gitkeep +0 -0
- data/templates/empty/objects/assistant.md +41 -0
- data/templates/minimal/.gitignore +5 -0
- data/templates/minimal/manifest.yml +7 -0
- data/templates/minimal/objects/assistant.md +41 -0
- data/templates/writer/.gitignore +5 -0
- data/templates/writer/manifest.yml +17 -0
- data/templates/writer/objects/coordinator.md +33 -0
- data/templates/writer/objects/editor.md +33 -0
- data/templates/writer/objects/researcher.md +34 -0
- metadata +343 -0
|
@@ -0,0 +1,174 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import type { PromptObject, Session, ThreadType } from '../types'
|
|
3
|
+
|
|
4
|
+
interface ThreadsSidebarProps {
|
|
5
|
+
po: PromptObject
|
|
6
|
+
switchSession: (target: string, sessionId: string) => void
|
|
7
|
+
createThread: (target: string) => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Build a flat list with depth info
|
|
11
|
+
interface ThreadItem {
|
|
12
|
+
session: Session
|
|
13
|
+
depth: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function buildThreadList(sessions: Session[]): ThreadItem[] {
|
|
17
|
+
const childrenMap = new Map<string, Session[]>()
|
|
18
|
+
|
|
19
|
+
// Index children
|
|
20
|
+
sessions.forEach((s) => {
|
|
21
|
+
if (s.parent_session_id) {
|
|
22
|
+
const children = childrenMap.get(s.parent_session_id) || []
|
|
23
|
+
children.push(s)
|
|
24
|
+
childrenMap.set(s.parent_session_id, children)
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
// Build flat list with depth
|
|
29
|
+
const result: ThreadItem[] = []
|
|
30
|
+
|
|
31
|
+
function traverse(session: Session, depth: number) {
|
|
32
|
+
result.push({ session, depth })
|
|
33
|
+
const children = childrenMap.get(session.id) || []
|
|
34
|
+
children
|
|
35
|
+
.sort((a, b) => (a.updated_at || '').localeCompare(b.updated_at || ''))
|
|
36
|
+
.forEach((child) => traverse(child, depth + 1))
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Get roots and traverse
|
|
40
|
+
const roots = sessions
|
|
41
|
+
.filter((s) => !s.parent_session_id)
|
|
42
|
+
.sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''))
|
|
43
|
+
|
|
44
|
+
roots.forEach((root) => traverse(root, 0))
|
|
45
|
+
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ThreadTypeIcon({ type }: { type: ThreadType }) {
|
|
50
|
+
switch (type) {
|
|
51
|
+
case 'delegation':
|
|
52
|
+
return <span className="text-purple-400" title="Delegation">↳</span>
|
|
53
|
+
case 'fork':
|
|
54
|
+
return <span className="text-blue-400" title="Fork">⑂</span>
|
|
55
|
+
case 'continuation':
|
|
56
|
+
return <span className="text-gray-400" title="Continuation">→</span>
|
|
57
|
+
default:
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function ThreadsSidebar({ po, switchSession, createThread }: ThreadsSidebarProps) {
|
|
63
|
+
const sessions = po.sessions || []
|
|
64
|
+
const currentSessionId = po.current_session?.id
|
|
65
|
+
|
|
66
|
+
const threadList = useMemo(() => buildThreadList(sessions), [sessions])
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="h-full flex flex-col">
|
|
70
|
+
<div className="p-3 border-b border-po-border flex items-center justify-between">
|
|
71
|
+
<h2 className="text-sm font-medium text-gray-400">Threads</h2>
|
|
72
|
+
<button
|
|
73
|
+
onClick={() => createThread(po.name)}
|
|
74
|
+
className="text-xs text-gray-500 hover:text-po-accent transition-colors"
|
|
75
|
+
title="New thread"
|
|
76
|
+
>
|
|
77
|
+
+ New
|
|
78
|
+
</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className="flex-1 overflow-auto p-2 space-y-1">
|
|
82
|
+
{threadList.length === 0 ? (
|
|
83
|
+
<div className="text-xs text-gray-500 text-center py-4">
|
|
84
|
+
No threads yet
|
|
85
|
+
</div>
|
|
86
|
+
) : (
|
|
87
|
+
threadList.map(({ session, depth }) => (
|
|
88
|
+
<button
|
|
89
|
+
key={session.id}
|
|
90
|
+
onClick={() => switchSession(po.name, session.id)}
|
|
91
|
+
className={`w-full text-left p-2 rounded text-xs transition-colors ${
|
|
92
|
+
session.id === currentSessionId
|
|
93
|
+
? 'bg-po-accent/20 border border-po-accent'
|
|
94
|
+
: 'hover:bg-po-surface border border-transparent'
|
|
95
|
+
}`}
|
|
96
|
+
style={{ paddingLeft: `${8 + depth * 12}px` }}
|
|
97
|
+
>
|
|
98
|
+
<div className="flex items-center gap-1">
|
|
99
|
+
{depth > 0 && <ThreadTypeIcon type={session.thread_type} />}
|
|
100
|
+
<span className={`truncate flex-1 ${session.id === currentSessionId ? 'text-white' : 'text-gray-300'}`}>
|
|
101
|
+
{session.name || `Thread ${session.id.slice(0, 6)}`}
|
|
102
|
+
</span>
|
|
103
|
+
{session.id === currentSessionId && (
|
|
104
|
+
<span className="w-1.5 h-1.5 rounded-full bg-po-accent flex-shrink-0" />
|
|
105
|
+
)}
|
|
106
|
+
</div>
|
|
107
|
+
<div className="text-gray-500 text-[10px] mt-0.5">
|
|
108
|
+
{session.message_count} msgs
|
|
109
|
+
{session.parent_po && (
|
|
110
|
+
<span className="text-purple-400 ml-1">from {session.parent_po}</span>
|
|
111
|
+
)}
|
|
112
|
+
</div>
|
|
113
|
+
</button>
|
|
114
|
+
))
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
</div>
|
|
118
|
+
)
|
|
119
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { Header } from './Header'
|
|
2
|
+
export { Dashboard } from './Dashboard'
|
|
3
|
+
export { POCard } from './POCard'
|
|
4
|
+
export { PODetail } from './PODetail'
|
|
5
|
+
export { ChatPanel } from './ChatPanel'
|
|
6
|
+
export { SessionsPanel } from './SessionsPanel'
|
|
7
|
+
export { CapabilitiesPanel } from './CapabilitiesPanel'
|
|
8
|
+
export { MessageBus } from './MessageBus'
|
|
9
|
+
export { NotificationPanel } from './NotificationPanel'
|
|
10
|
+
export { MarkdownMessage } from './MarkdownMessage'
|
|
11
|
+
export { PromptPanel } from './PromptPanel'
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { useEffect, useRef, useCallback } from 'react'
|
|
2
|
+
import { useStore } from '../store'
|
|
3
|
+
import type {
|
|
4
|
+
WSMessage,
|
|
5
|
+
PromptObject,
|
|
6
|
+
BusMessage,
|
|
7
|
+
Notification,
|
|
8
|
+
Environment,
|
|
9
|
+
Message,
|
|
10
|
+
LLMConfig,
|
|
11
|
+
SendMessagePayload,
|
|
12
|
+
RespondToNotificationPayload,
|
|
13
|
+
CreateSessionPayload,
|
|
14
|
+
SwitchSessionPayload,
|
|
15
|
+
CreateThreadPayload,
|
|
16
|
+
ThreadType,
|
|
17
|
+
} from '../types'
|
|
18
|
+
|
|
19
|
+
export function useWebSocket() {
|
|
20
|
+
const ws = useRef<WebSocket | null>(null)
|
|
21
|
+
const reconnectTimeout = useRef<number | null>(null)
|
|
22
|
+
|
|
23
|
+
const {
|
|
24
|
+
setConnected,
|
|
25
|
+
setEnvironment,
|
|
26
|
+
setPromptObject,
|
|
27
|
+
removePromptObject,
|
|
28
|
+
updateSessionMessages,
|
|
29
|
+
switchPOSession,
|
|
30
|
+
addBusMessage,
|
|
31
|
+
addNotification,
|
|
32
|
+
removeNotification,
|
|
33
|
+
appendStreamChunk,
|
|
34
|
+
clearStream,
|
|
35
|
+
setPendingResponse,
|
|
36
|
+
clearPendingResponse,
|
|
37
|
+
setLLMConfig,
|
|
38
|
+
updateCurrentLLM,
|
|
39
|
+
} = useStore()
|
|
40
|
+
|
|
41
|
+
const connect = useCallback(() => {
|
|
42
|
+
// Determine WebSocket URL
|
|
43
|
+
// In dev mode (Vite on 5173), connect directly to Ruby server on 3000
|
|
44
|
+
// In production, connect to same host
|
|
45
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
|
46
|
+
const isDev = window.location.port === '5173'
|
|
47
|
+
const host = isDev ? 'localhost:3000' : window.location.host
|
|
48
|
+
const wsUrl = `${protocol}//${host}`
|
|
49
|
+
|
|
50
|
+
console.log('Connecting to WebSocket:', wsUrl)
|
|
51
|
+
ws.current = new WebSocket(wsUrl)
|
|
52
|
+
|
|
53
|
+
ws.current.onopen = () => {
|
|
54
|
+
console.log('WebSocket connected')
|
|
55
|
+
setConnected(true)
|
|
56
|
+
|
|
57
|
+
// Clear any pending reconnect
|
|
58
|
+
if (reconnectTimeout.current) {
|
|
59
|
+
clearTimeout(reconnectTimeout.current)
|
|
60
|
+
reconnectTimeout.current = null
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ws.current.onclose = () => {
|
|
65
|
+
console.log('WebSocket disconnected')
|
|
66
|
+
setConnected(false)
|
|
67
|
+
|
|
68
|
+
// Attempt to reconnect after 2 seconds
|
|
69
|
+
reconnectTimeout.current = window.setTimeout(() => {
|
|
70
|
+
console.log('Attempting to reconnect...')
|
|
71
|
+
connect()
|
|
72
|
+
}, 2000)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
ws.current.onerror = (error) => {
|
|
76
|
+
console.error('WebSocket error:', error)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
ws.current.onmessage = (event) => {
|
|
80
|
+
try {
|
|
81
|
+
const message: WSMessage = JSON.parse(event.data)
|
|
82
|
+
handleMessage(message)
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error('Failed to parse WebSocket message:', error)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}, [setConnected])
|
|
88
|
+
|
|
89
|
+
const handleMessage = useCallback(
|
|
90
|
+
(message: WSMessage) => {
|
|
91
|
+
switch (message.type) {
|
|
92
|
+
case 'environment':
|
|
93
|
+
setEnvironment(message.payload as Environment)
|
|
94
|
+
break
|
|
95
|
+
|
|
96
|
+
case 'po_state': {
|
|
97
|
+
const { name, state } = message.payload as {
|
|
98
|
+
name: string
|
|
99
|
+
state: Partial<PromptObject>
|
|
100
|
+
}
|
|
101
|
+
setPromptObject(name, state)
|
|
102
|
+
break
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'po_response': {
|
|
106
|
+
const { target, content } = message.payload as {
|
|
107
|
+
target: string
|
|
108
|
+
content: string
|
|
109
|
+
}
|
|
110
|
+
setPendingResponse(target, content)
|
|
111
|
+
// Clear after a short delay to allow UI to update
|
|
112
|
+
setTimeout(() => clearPendingResponse(target), 100)
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
case 'stream': {
|
|
117
|
+
const { target, chunk } = message.payload as {
|
|
118
|
+
target: string
|
|
119
|
+
chunk: string
|
|
120
|
+
}
|
|
121
|
+
appendStreamChunk(target, chunk)
|
|
122
|
+
break
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
case 'stream_end': {
|
|
126
|
+
const { target } = message.payload as { target: string }
|
|
127
|
+
clearStream(target)
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'bus_message':
|
|
132
|
+
addBusMessage(message.payload as BusMessage)
|
|
133
|
+
break
|
|
134
|
+
|
|
135
|
+
case 'notification':
|
|
136
|
+
addNotification(message.payload as Notification)
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
case 'notification_resolved': {
|
|
140
|
+
const { id } = message.payload as { id: string }
|
|
141
|
+
removeNotification(id)
|
|
142
|
+
break
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Live file updates
|
|
146
|
+
case 'po_added': {
|
|
147
|
+
const { name, state } = message.payload as {
|
|
148
|
+
name: string
|
|
149
|
+
state: Partial<PromptObject>
|
|
150
|
+
}
|
|
151
|
+
console.log('PO added:', name)
|
|
152
|
+
setPromptObject(name, state)
|
|
153
|
+
break
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
case 'po_modified': {
|
|
157
|
+
const { name, state } = message.payload as {
|
|
158
|
+
name: string
|
|
159
|
+
state: Partial<PromptObject>
|
|
160
|
+
}
|
|
161
|
+
console.log('PO modified:', name)
|
|
162
|
+
setPromptObject(name, state)
|
|
163
|
+
break
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
case 'po_removed': {
|
|
167
|
+
const { name } = message.payload as { name: string }
|
|
168
|
+
console.log('PO removed:', name)
|
|
169
|
+
removePromptObject(name)
|
|
170
|
+
break
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'session_updated': {
|
|
174
|
+
const { target, session_id, messages } = message.payload as {
|
|
175
|
+
target: string
|
|
176
|
+
session_id: string
|
|
177
|
+
messages: Message[]
|
|
178
|
+
}
|
|
179
|
+
updateSessionMessages(target, session_id, messages)
|
|
180
|
+
break
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
case 'thread_created': {
|
|
184
|
+
const { target, thread_id, thread_type } = message.payload as {
|
|
185
|
+
target: string
|
|
186
|
+
thread_id: string
|
|
187
|
+
name: string | null
|
|
188
|
+
thread_type: ThreadType
|
|
189
|
+
}
|
|
190
|
+
console.log('Thread created:', target, thread_id, thread_type)
|
|
191
|
+
// IMMEDIATELY switch to the new thread so user sees their message
|
|
192
|
+
// This ensures session_updated messages for this thread are displayed
|
|
193
|
+
switchPOSession(target, thread_id)
|
|
194
|
+
break
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case 'thread_tree': {
|
|
198
|
+
// Thread tree response - could be used for navigation
|
|
199
|
+
console.log('Thread tree received:', message.payload)
|
|
200
|
+
break
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
case 'llm_config':
|
|
204
|
+
setLLMConfig(message.payload as LLMConfig)
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
case 'llm_switched': {
|
|
208
|
+
const { provider, model } = message.payload as {
|
|
209
|
+
provider: string
|
|
210
|
+
model: string
|
|
211
|
+
}
|
|
212
|
+
updateCurrentLLM(provider, model)
|
|
213
|
+
break
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
case 'error': {
|
|
217
|
+
const { message: errorMsg } = message.payload as { message: string }
|
|
218
|
+
console.error('Server error:', errorMsg)
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
case 'pong':
|
|
223
|
+
// Heartbeat response, ignore
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
default:
|
|
227
|
+
console.log('Unknown message type:', message.type)
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
[
|
|
231
|
+
setEnvironment,
|
|
232
|
+
setPromptObject,
|
|
233
|
+
removePromptObject,
|
|
234
|
+
updateSessionMessages,
|
|
235
|
+
switchPOSession,
|
|
236
|
+
setPendingResponse,
|
|
237
|
+
clearPendingResponse,
|
|
238
|
+
appendStreamChunk,
|
|
239
|
+
clearStream,
|
|
240
|
+
addBusMessage,
|
|
241
|
+
addNotification,
|
|
242
|
+
removeNotification,
|
|
243
|
+
setLLMConfig,
|
|
244
|
+
updateCurrentLLM,
|
|
245
|
+
]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
// Connect on mount
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
connect()
|
|
251
|
+
|
|
252
|
+
return () => {
|
|
253
|
+
if (reconnectTimeout.current) {
|
|
254
|
+
clearTimeout(reconnectTimeout.current)
|
|
255
|
+
}
|
|
256
|
+
ws.current?.close()
|
|
257
|
+
}
|
|
258
|
+
}, [connect])
|
|
259
|
+
|
|
260
|
+
// Send message to a PO
|
|
261
|
+
const sendMessage = useCallback((target: string, content: string, newThread?: boolean) => {
|
|
262
|
+
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
|
|
263
|
+
console.error('WebSocket not connected')
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const payload: SendMessagePayload = { target, content, new_thread: newThread }
|
|
268
|
+
ws.current.send(
|
|
269
|
+
JSON.stringify({
|
|
270
|
+
type: 'send_message',
|
|
271
|
+
payload,
|
|
272
|
+
})
|
|
273
|
+
)
|
|
274
|
+
}, [])
|
|
275
|
+
|
|
276
|
+
// Respond to a notification
|
|
277
|
+
const respondToNotification = useCallback((id: string, response: string) => {
|
|
278
|
+
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
|
|
279
|
+
console.error('WebSocket not connected')
|
|
280
|
+
return
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const payload: RespondToNotificationPayload = { id, response }
|
|
284
|
+
ws.current.send(
|
|
285
|
+
JSON.stringify({
|
|
286
|
+
type: 'respond_to_notification',
|
|
287
|
+
payload,
|
|
288
|
+
})
|
|
289
|
+
)
|
|
290
|
+
}, [])
|
|
291
|
+
|
|
292
|
+
// Create a new session
|
|
293
|
+
const createSession = useCallback((target: string, name?: string) => {
|
|
294
|
+
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
|
|
295
|
+
console.error('WebSocket not connected')
|
|
296
|
+
return
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const payload: CreateSessionPayload = { target, name }
|
|
300
|
+
ws.current.send(
|
|
301
|
+
JSON.stringify({
|
|
302
|
+
type: 'create_session',
|
|
303
|
+
payload,
|
|
304
|
+
})
|
|
305
|
+
)
|
|
306
|
+
}, [])
|
|
307
|
+
|
|
308
|
+
// Switch to a different session
|
|
309
|
+
const switchSession = useCallback((target: string, sessionId: string) => {
|
|
310
|
+
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
|
|
311
|
+
console.error('WebSocket not connected')
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const payload: SwitchSessionPayload = { target, session_id: sessionId }
|
|
316
|
+
ws.current.send(
|
|
317
|
+
JSON.stringify({
|
|
318
|
+
type: 'switch_session',
|
|
319
|
+
payload,
|
|
320
|
+
})
|
|
321
|
+
)
|
|
322
|
+
}, [])
|
|
323
|
+
|
|
324
|
+
// Switch LLM provider/model
|
|
325
|
+
const switchLLM = useCallback((provider: string, model?: string) => {
|
|
326
|
+
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
|
|
327
|
+
console.error('WebSocket not connected')
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
ws.current.send(
|
|
332
|
+
JSON.stringify({
|
|
333
|
+
type: 'switch_llm',
|
|
334
|
+
payload: { provider, model },
|
|
335
|
+
})
|
|
336
|
+
)
|
|
337
|
+
}, [])
|
|
338
|
+
|
|
339
|
+
// Create a new thread (defaults to root thread)
|
|
340
|
+
const createThread = useCallback((target: string, name?: string, threadType?: ThreadType) => {
|
|
341
|
+
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
|
|
342
|
+
console.error('WebSocket not connected')
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const payload: CreateThreadPayload = { target, name, thread_type: threadType }
|
|
347
|
+
ws.current.send(
|
|
348
|
+
JSON.stringify({
|
|
349
|
+
type: 'create_thread',
|
|
350
|
+
payload,
|
|
351
|
+
})
|
|
352
|
+
)
|
|
353
|
+
}, [])
|
|
354
|
+
|
|
355
|
+
return {
|
|
356
|
+
sendMessage,
|
|
357
|
+
respondToNotification,
|
|
358
|
+
createSession,
|
|
359
|
+
switchSession,
|
|
360
|
+
switchLLM,
|
|
361
|
+
createThread,
|
|
362
|
+
}
|
|
363
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
@tailwind base;
|
|
2
|
+
@tailwind components;
|
|
3
|
+
@tailwind utilities;
|
|
4
|
+
|
|
5
|
+
/* Base styles */
|
|
6
|
+
html {
|
|
7
|
+
color-scheme: dark;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
body {
|
|
11
|
+
@apply antialiased;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/* Custom scrollbar */
|
|
15
|
+
::-webkit-scrollbar {
|
|
16
|
+
width: 8px;
|
|
17
|
+
height: 8px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
::-webkit-scrollbar-track {
|
|
21
|
+
@apply bg-po-bg;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
::-webkit-scrollbar-thumb {
|
|
25
|
+
@apply bg-po-border rounded-full;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
::-webkit-scrollbar-thumb:hover {
|
|
29
|
+
@apply bg-po-accent;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Custom utilities */
|
|
33
|
+
@layer utilities {
|
|
34
|
+
.scrollbar-thin {
|
|
35
|
+
scrollbar-width: thin;
|
|
36
|
+
}
|
|
37
|
+
}
|