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.
Files changed (117) hide show
  1. checksums.yaml +7 -0
  2. data/CLAUDE.md +108 -0
  3. data/Gemfile +10 -0
  4. data/Gemfile.lock +231 -0
  5. data/IMPLEMENTATION_PLAN.md +1073 -0
  6. data/LICENSE +21 -0
  7. data/README.md +73 -0
  8. data/Rakefile +27 -0
  9. data/design-doc-v2.md +1232 -0
  10. data/exe/prompt_objects +572 -0
  11. data/exe/prompt_objects_mcp +34 -0
  12. data/frontend/.gitignore +3 -0
  13. data/frontend/index.html +13 -0
  14. data/frontend/package-lock.json +4417 -0
  15. data/frontend/package.json +32 -0
  16. data/frontend/postcss.config.js +6 -0
  17. data/frontend/src/App.tsx +95 -0
  18. data/frontend/src/components/CapabilitiesPanel.tsx +44 -0
  19. data/frontend/src/components/ChatPanel.tsx +251 -0
  20. data/frontend/src/components/Dashboard.tsx +83 -0
  21. data/frontend/src/components/Header.tsx +141 -0
  22. data/frontend/src/components/MarkdownMessage.tsx +153 -0
  23. data/frontend/src/components/MessageBus.tsx +55 -0
  24. data/frontend/src/components/ModelSelector.tsx +112 -0
  25. data/frontend/src/components/NotificationPanel.tsx +134 -0
  26. data/frontend/src/components/POCard.tsx +56 -0
  27. data/frontend/src/components/PODetail.tsx +117 -0
  28. data/frontend/src/components/PromptPanel.tsx +51 -0
  29. data/frontend/src/components/SessionsPanel.tsx +174 -0
  30. data/frontend/src/components/ThreadsSidebar.tsx +119 -0
  31. data/frontend/src/components/index.ts +11 -0
  32. data/frontend/src/hooks/useWebSocket.ts +363 -0
  33. data/frontend/src/index.css +37 -0
  34. data/frontend/src/main.tsx +10 -0
  35. data/frontend/src/store/index.ts +246 -0
  36. data/frontend/src/types/index.ts +146 -0
  37. data/frontend/tailwind.config.js +25 -0
  38. data/frontend/tsconfig.json +30 -0
  39. data/frontend/vite.config.ts +29 -0
  40. data/lib/prompt_objects/capability.rb +46 -0
  41. data/lib/prompt_objects/cli.rb +431 -0
  42. data/lib/prompt_objects/connectors/base.rb +73 -0
  43. data/lib/prompt_objects/connectors/mcp.rb +524 -0
  44. data/lib/prompt_objects/environment/exporter.rb +83 -0
  45. data/lib/prompt_objects/environment/git.rb +118 -0
  46. data/lib/prompt_objects/environment/importer.rb +159 -0
  47. data/lib/prompt_objects/environment/manager.rb +401 -0
  48. data/lib/prompt_objects/environment/manifest.rb +218 -0
  49. data/lib/prompt_objects/environment.rb +283 -0
  50. data/lib/prompt_objects/human_queue.rb +144 -0
  51. data/lib/prompt_objects/llm/anthropic_adapter.rb +137 -0
  52. data/lib/prompt_objects/llm/factory.rb +84 -0
  53. data/lib/prompt_objects/llm/gemini_adapter.rb +209 -0
  54. data/lib/prompt_objects/llm/openai_adapter.rb +104 -0
  55. data/lib/prompt_objects/llm/response.rb +61 -0
  56. data/lib/prompt_objects/loader.rb +32 -0
  57. data/lib/prompt_objects/mcp/server.rb +167 -0
  58. data/lib/prompt_objects/mcp/tools/get_conversation.rb +60 -0
  59. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +54 -0
  60. data/lib/prompt_objects/mcp/tools/inspect_po.rb +73 -0
  61. data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +37 -0
  62. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +68 -0
  63. data/lib/prompt_objects/mcp/tools/send_message.rb +71 -0
  64. data/lib/prompt_objects/message_bus.rb +97 -0
  65. data/lib/prompt_objects/primitive.rb +13 -0
  66. data/lib/prompt_objects/primitives/http_get.rb +72 -0
  67. data/lib/prompt_objects/primitives/list_files.rb +95 -0
  68. data/lib/prompt_objects/primitives/read_file.rb +81 -0
  69. data/lib/prompt_objects/primitives/write_file.rb +73 -0
  70. data/lib/prompt_objects/prompt_object.rb +415 -0
  71. data/lib/prompt_objects/registry.rb +88 -0
  72. data/lib/prompt_objects/server/api/routes.rb +297 -0
  73. data/lib/prompt_objects/server/app.rb +174 -0
  74. data/lib/prompt_objects/server/file_watcher.rb +113 -0
  75. data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +77 -0
  76. data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +1 -0
  77. data/lib/prompt_objects/server/public/index.html +14 -0
  78. data/lib/prompt_objects/server/websocket_handler.rb +619 -0
  79. data/lib/prompt_objects/server.rb +166 -0
  80. data/lib/prompt_objects/session/store.rb +826 -0
  81. data/lib/prompt_objects/universal/add_capability.rb +74 -0
  82. data/lib/prompt_objects/universal/add_primitive.rb +113 -0
  83. data/lib/prompt_objects/universal/ask_human.rb +109 -0
  84. data/lib/prompt_objects/universal/create_capability.rb +219 -0
  85. data/lib/prompt_objects/universal/create_primitive.rb +170 -0
  86. data/lib/prompt_objects/universal/list_capabilities.rb +55 -0
  87. data/lib/prompt_objects/universal/list_primitives.rb +145 -0
  88. data/lib/prompt_objects/universal/modify_primitive.rb +180 -0
  89. data/lib/prompt_objects/universal/request_primitive.rb +287 -0
  90. data/lib/prompt_objects/universal/think.rb +41 -0
  91. data/lib/prompt_objects/universal/verify_primitive.rb +173 -0
  92. data/lib/prompt_objects.rb +62 -0
  93. data/objects/coordinator.md +48 -0
  94. data/objects/greeter.md +30 -0
  95. data/objects/reader.md +33 -0
  96. data/prompt_objects.gemspec +50 -0
  97. data/templates/basic/.gitignore +2 -0
  98. data/templates/basic/manifest.yml +7 -0
  99. data/templates/basic/objects/basic.md +32 -0
  100. data/templates/developer/.gitignore +5 -0
  101. data/templates/developer/manifest.yml +17 -0
  102. data/templates/developer/objects/code_reviewer.md +33 -0
  103. data/templates/developer/objects/coordinator.md +39 -0
  104. data/templates/developer/objects/debugger.md +35 -0
  105. data/templates/empty/.gitignore +5 -0
  106. data/templates/empty/manifest.yml +14 -0
  107. data/templates/empty/objects/.gitkeep +0 -0
  108. data/templates/empty/objects/assistant.md +41 -0
  109. data/templates/minimal/.gitignore +5 -0
  110. data/templates/minimal/manifest.yml +7 -0
  111. data/templates/minimal/objects/assistant.md +41 -0
  112. data/templates/writer/.gitignore +5 -0
  113. data/templates/writer/manifest.yml +17 -0
  114. data/templates/writer/objects/coordinator.md +33 -0
  115. data/templates/writer/objects/editor.md +33 -0
  116. data/templates/writer/objects/researcher.md +34 -0
  117. metadata +343 -0
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "prompt-objects-frontend",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc && vite build",
9
+ "preview": "vite preview",
10
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
11
+ },
12
+ "dependencies": {
13
+ "@types/react-syntax-highlighter": "^15.5.13",
14
+ "clsx": "^2.1.1",
15
+ "react": "^18.3.1",
16
+ "react-dom": "^18.3.1",
17
+ "react-markdown": "^10.1.0",
18
+ "react-syntax-highlighter": "^16.1.0",
19
+ "remark-gfm": "^4.0.1",
20
+ "zustand": "^5.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@types/react": "^18.3.12",
24
+ "@types/react-dom": "^18.3.1",
25
+ "@vitejs/plugin-react": "^4.3.4",
26
+ "autoprefixer": "^10.4.20",
27
+ "postcss": "^8.4.49",
28
+ "tailwindcss": "^3.4.16",
29
+ "typescript": "^5.6.3",
30
+ "vite": "^6.0.3"
31
+ }
32
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
@@ -0,0 +1,95 @@
1
+ import { useState } from 'react'
2
+ import { useWebSocket } from './hooks/useWebSocket'
3
+ import { useStore, useSelectedPO } from './store'
4
+ import { Header } from './components/Header'
5
+ import { Dashboard } from './components/Dashboard'
6
+ import { PODetail } from './components/PODetail'
7
+ import { MessageBus } from './components/MessageBus'
8
+ import { NotificationPanel } from './components/NotificationPanel'
9
+ import { ThreadsSidebar } from './components/ThreadsSidebar'
10
+
11
+ export default function App() {
12
+ const { sendMessage, respondToNotification, createSession, switchSession, switchLLM, createThread } =
13
+ useWebSocket()
14
+ const { selectedPO, busOpen, notifications } = useStore()
15
+ const selectedPOData = useSelectedPO()
16
+ const [splitView, setSplitView] = useState(true) // Default to split view
17
+
18
+ return (
19
+ <div className="h-screen flex flex-col bg-po-bg">
20
+ <Header switchLLM={switchLLM} />
21
+
22
+ <div className="flex-1 flex overflow-hidden">
23
+ {/* Split view: Dashboard sidebar on left when PO selected */}
24
+ {splitView && selectedPO && (
25
+ <>
26
+ {/* PO List */}
27
+ <aside className="w-56 border-r border-po-border bg-po-surface overflow-hidden flex flex-col">
28
+ <div className="p-3 border-b border-po-border flex items-center justify-between">
29
+ <h2 className="text-sm font-medium text-gray-400">Prompt Objects</h2>
30
+ <button
31
+ onClick={() => setSplitView(false)}
32
+ className="text-xs text-gray-500 hover:text-white"
33
+ title="Hide sidebar"
34
+ >
35
+
36
+ </button>
37
+ </div>
38
+ <div className="flex-1 overflow-auto">
39
+ <Dashboard compact />
40
+ </div>
41
+ </aside>
42
+
43
+ {/* Threads List for selected PO */}
44
+ {selectedPOData && (
45
+ <aside className="w-56 border-r border-po-border bg-po-bg overflow-hidden">
46
+ <ThreadsSidebar
47
+ po={selectedPOData}
48
+ switchSession={switchSession}
49
+ createThread={createThread}
50
+ />
51
+ </aside>
52
+ )}
53
+ </>
54
+ )}
55
+
56
+ {/* Main content */}
57
+ <main className="flex-1 overflow-hidden flex flex-col">
58
+ {/* Show expand button when sidebar is hidden */}
59
+ {!splitView && selectedPO && (
60
+ <button
61
+ onClick={() => setSplitView(true)}
62
+ className="absolute left-2 top-16 z-10 bg-po-surface border border-po-border rounded px-2 py-1 text-xs text-gray-400 hover:text-white hover:border-po-accent transition-colors"
63
+ title="Show dashboard sidebar"
64
+ >
65
+ ☰ POs
66
+ </button>
67
+ )}
68
+
69
+ {selectedPO ? (
70
+ <PODetail
71
+ sendMessage={sendMessage}
72
+ createSession={createSession}
73
+ switchSession={switchSession}
74
+ createThread={createThread}
75
+ />
76
+ ) : (
77
+ <Dashboard />
78
+ )}
79
+ </main>
80
+
81
+ {/* Message Bus sidebar */}
82
+ {busOpen && (
83
+ <aside className="w-80 border-l border-po-border bg-po-surface overflow-hidden">
84
+ <MessageBus />
85
+ </aside>
86
+ )}
87
+ </div>
88
+
89
+ {/* Notification panel */}
90
+ {notifications.length > 0 && (
91
+ <NotificationPanel respondToNotification={respondToNotification} />
92
+ )}
93
+ </div>
94
+ )
95
+ }
@@ -0,0 +1,44 @@
1
+ import type { PromptObject } from '../types'
2
+
3
+ interface CapabilitiesPanelProps {
4
+ po: PromptObject
5
+ }
6
+
7
+ export function CapabilitiesPanel({ po }: CapabilitiesPanelProps) {
8
+ const capabilities = po.capabilities || []
9
+
10
+ return (
11
+ <div className="h-full overflow-auto p-4">
12
+ <h3 className="text-lg font-medium text-white mb-4">Capabilities</h3>
13
+
14
+ {capabilities.length === 0 ? (
15
+ <div className="text-gray-500 text-center py-8">
16
+ No capabilities defined.
17
+ </div>
18
+ ) : (
19
+ <div className="space-y-2">
20
+ {capabilities.map((cap, index) => (
21
+ <div
22
+ key={index}
23
+ className="bg-po-surface border border-po-border rounded-lg p-3"
24
+ >
25
+ <div className="font-mono text-sm text-po-accent">{cap}</div>
26
+ </div>
27
+ ))}
28
+ </div>
29
+ )}
30
+
31
+ <div className="mt-6 p-4 bg-po-bg rounded-lg border border-po-border">
32
+ <h4 className="text-sm font-medium text-gray-400 mb-2">
33
+ Universal Capabilities
34
+ </h4>
35
+ <p className="text-xs text-gray-500">
36
+ All Prompt Objects automatically have access to universal capabilities
37
+ like <code className="text-po-accent">ask_human</code>,{' '}
38
+ <code className="text-po-accent">think</code>, and{' '}
39
+ <code className="text-po-accent">request_capability</code>.
40
+ </p>
41
+ </div>
42
+ </div>
43
+ )
44
+ }
@@ -0,0 +1,251 @@
1
+ import { useState, useRef, useEffect } from 'react'
2
+ import { useStore } from '../store'
3
+ import { MarkdownMessage } from './MarkdownMessage'
4
+ import type { PromptObject, Message, ToolCall } from '../types'
5
+
6
+ interface ChatPanelProps {
7
+ po: PromptObject
8
+ sendMessage: (target: string, content: string, newThread?: boolean) => void
9
+ }
10
+
11
+ export function ChatPanel({ po, sendMessage }: ChatPanelProps) {
12
+ const [input, setInput] = useState('')
13
+ const [continueThread, setContinueThread] = useState(false)
14
+ const messagesEndRef = useRef<HTMLDivElement>(null)
15
+ const { streamingContent } = useStore()
16
+
17
+ const messages = po.current_session?.messages || []
18
+ const streaming = streamingContent[po.name]
19
+ const hasMessages = messages.length > 0
20
+
21
+ useEffect(() => {
22
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
23
+ }, [messages, streaming])
24
+
25
+ const handleSubmit = (e: React.FormEvent) => {
26
+ e.preventDefault()
27
+ if (!input.trim()) return
28
+
29
+ const content = input.trim()
30
+
31
+ // Determine if we should create a new thread
32
+ const shouldCreateNewThread = !continueThread && hasMessages
33
+
34
+ // Send message to server - it will handle thread creation + message in one operation
35
+ // Server sends immediate session_updated with user message for instant feedback
36
+ sendMessage(po.name, content, shouldCreateNewThread)
37
+ setInput('')
38
+ }
39
+
40
+ return (
41
+ <div className="h-full flex flex-col">
42
+ {/* Messages */}
43
+ <div className="flex-1 overflow-auto p-4 space-y-4">
44
+ {messages.length === 0 && !streaming && (
45
+ <div className="h-full flex items-center justify-center text-gray-500">
46
+ <div className="text-center">
47
+ <div className="text-2xl mb-2">💬</div>
48
+ <div>Start a conversation with {po.name}</div>
49
+ </div>
50
+ </div>
51
+ )}
52
+
53
+ {messages.map((message, index) => (
54
+ <MessageBubble key={index} message={message} />
55
+ ))}
56
+
57
+ {/* Streaming content */}
58
+ {streaming && (
59
+ <div className="flex gap-3">
60
+ <div className="w-8 h-8 rounded-full bg-po-accent flex items-center justify-center text-white text-sm font-medium flex-shrink-0">
61
+ AI
62
+ </div>
63
+ <div className="flex-1 bg-po-surface rounded-lg p-3 text-gray-200">
64
+ <MarkdownMessage content={streaming} />
65
+ <span className="inline-block w-2 h-4 bg-po-accent animate-pulse ml-1" />
66
+ </div>
67
+ </div>
68
+ )}
69
+
70
+ <div ref={messagesEndRef} />
71
+ </div>
72
+
73
+ {/* Input */}
74
+ <form onSubmit={handleSubmit} className="border-t border-po-border p-4">
75
+ <div className="flex gap-3">
76
+ <input
77
+ type="text"
78
+ value={input}
79
+ onChange={(e) => setInput(e.target.value)}
80
+ placeholder={`Message ${po.name}...`}
81
+ className="flex-1 bg-po-surface border border-po-border rounded-lg px-4 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-po-accent"
82
+ disabled={po.status !== 'idle'}
83
+ />
84
+ <button
85
+ type="submit"
86
+ disabled={!input.trim() || po.status !== 'idle'}
87
+ className="px-4 py-2 bg-po-accent text-white rounded-lg font-medium hover:bg-po-accent/80 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
88
+ >
89
+ Send
90
+ </button>
91
+ </div>
92
+
93
+ {/* Thread toggle - only show when there are existing messages */}
94
+ {hasMessages && (
95
+ <div className="flex items-center gap-3 mt-2 text-xs">
96
+ <button
97
+ type="button"
98
+ onClick={() => setContinueThread(false)}
99
+ className={`px-2 py-1 rounded transition-colors ${
100
+ !continueThread
101
+ ? 'bg-po-accent/20 text-po-accent border border-po-accent'
102
+ : 'text-gray-400 hover:text-white'
103
+ }`}
104
+ >
105
+ New thread
106
+ </button>
107
+ <button
108
+ type="button"
109
+ onClick={() => setContinueThread(true)}
110
+ className={`px-2 py-1 rounded transition-colors ${
111
+ continueThread
112
+ ? 'bg-po-accent/20 text-po-accent border border-po-accent'
113
+ : 'text-gray-400 hover:text-white'
114
+ }`}
115
+ >
116
+ Continue thread
117
+ </button>
118
+ <span className="text-gray-500">
119
+ {continueThread
120
+ ? 'Will add to current conversation'
121
+ : 'Will start a fresh conversation'}
122
+ </span>
123
+ </div>
124
+ )}
125
+ </form>
126
+ </div>
127
+ )
128
+ }
129
+
130
+ function MessageBubble({ message }: { message: Message }) {
131
+ const isUser = message.role === 'user'
132
+ const isAssistant = message.role === 'assistant'
133
+ const isTool = message.role === 'tool'
134
+
135
+ if (isTool) {
136
+ return (
137
+ <div className="text-xs text-gray-500 bg-po-bg rounded p-2 font-mono">
138
+ <div className="text-gray-400 mb-1">Tool Result:</div>
139
+ <div className="whitespace-pre-wrap break-all">
140
+ {message.content?.slice(0, 500)}
141
+ {message.content && message.content.length > 500 && '...'}
142
+ </div>
143
+ </div>
144
+ )
145
+ }
146
+
147
+ return (
148
+ <div className={`flex gap-3 ${isUser ? 'flex-row-reverse' : ''}`}>
149
+ <div
150
+ className={`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium flex-shrink-0 ${
151
+ isUser ? 'bg-po-border text-white' : 'bg-po-accent text-white'
152
+ }`}
153
+ >
154
+ {isUser ? 'You' : 'AI'}
155
+ </div>
156
+ <div
157
+ className={`flex-1 max-w-[80%] rounded-lg p-3 ${
158
+ isUser
159
+ ? 'bg-po-accent text-white'
160
+ : 'bg-po-surface text-gray-200'
161
+ }`}
162
+ >
163
+ {message.content && (
164
+ isAssistant ? (
165
+ <MarkdownMessage content={message.content} />
166
+ ) : (
167
+ <div className="whitespace-pre-wrap">{message.content}</div>
168
+ )
169
+ )}
170
+
171
+ {/* Tool calls */}
172
+ {isAssistant && message.tool_calls && message.tool_calls.length > 0 && (
173
+ <div className="mt-2 space-y-2">
174
+ {message.tool_calls.map((tc) => (
175
+ <ToolCallDisplay key={tc.id} toolCall={tc} />
176
+ ))}
177
+ </div>
178
+ )}
179
+ </div>
180
+ </div>
181
+ )
182
+ }
183
+
184
+ function ToolCallDisplay({ toolCall }: { toolCall: ToolCall }) {
185
+ const [expanded, setExpanded] = useState(false)
186
+ const { notifications } = useStore()
187
+
188
+ // Special display for ask_human
189
+ if (toolCall.name === 'ask_human') {
190
+ const question = toolCall.arguments.question as string
191
+ const options = toolCall.arguments.options as string[] | undefined
192
+
193
+ // Check if there's a pending notification for this (by matching question)
194
+ const isPending = notifications.some((n) => n.message === question)
195
+
196
+ return (
197
+ <div className="bg-po-warning/10 border border-po-warning/30 rounded-lg p-3">
198
+ <div className="flex items-center gap-2 mb-2">
199
+ <span className="text-po-warning text-sm font-medium">
200
+ Waiting for human input
201
+ </span>
202
+ {isPending ? (
203
+ <span className="text-xs bg-po-warning text-black px-2 py-0.5 rounded animate-pulse">
204
+ PENDING
205
+ </span>
206
+ ) : (
207
+ <span className="text-xs bg-green-600 text-white px-2 py-0.5 rounded">
208
+ RESOLVED
209
+ </span>
210
+ )}
211
+ </div>
212
+ <p className="text-gray-200 text-sm mb-2">{question}</p>
213
+ {options && options.length > 0 && (
214
+ <div className="flex flex-wrap gap-2">
215
+ {options.map((opt, i) => (
216
+ <span
217
+ key={i}
218
+ className="text-xs bg-po-bg px-2 py-1 rounded text-gray-400"
219
+ >
220
+ {opt}
221
+ </span>
222
+ ))}
223
+ </div>
224
+ )}
225
+ </div>
226
+ )
227
+ }
228
+
229
+ // Default display for other tool calls - expandable
230
+ return (
231
+ <div className="text-xs bg-po-bg/50 rounded overflow-hidden">
232
+ <button
233
+ onClick={() => setExpanded(!expanded)}
234
+ className="w-full px-2 py-1 text-left font-mono flex items-center gap-1 hover:bg-po-bg/70 transition-colors"
235
+ >
236
+ <span className="text-gray-500">{expanded ? '▼' : '▶'}</span>
237
+ <span className="text-po-accent">{toolCall.name}</span>
238
+ <span className="text-gray-500">
239
+ ({Object.keys(toolCall.arguments).length} args)
240
+ </span>
241
+ </button>
242
+ {expanded && (
243
+ <div className="px-2 pb-2 border-t border-po-border/50">
244
+ <pre className="text-gray-400 whitespace-pre-wrap break-all mt-1 text-[10px] leading-relaxed">
245
+ {JSON.stringify(toolCall.arguments, null, 2)}
246
+ </pre>
247
+ </div>
248
+ )}
249
+ </div>
250
+ )
251
+ }
@@ -0,0 +1,83 @@
1
+ import { usePromptObjects, useStore, usePONotifications } from '../store'
2
+ import { POCard } from './POCard'
3
+ import type { PromptObject } from '../types'
4
+
5
+ interface DashboardProps {
6
+ compact?: boolean
7
+ }
8
+
9
+ export function Dashboard({ compact = false }: DashboardProps) {
10
+ const promptObjects = usePromptObjects()
11
+
12
+ if (promptObjects.length === 0) {
13
+ return (
14
+ <div className="h-full flex items-center justify-center text-gray-500">
15
+ <div className="text-center">
16
+ <div className="text-4xl mb-4">🔮</div>
17
+ <div className="text-lg">No Prompt Objects loaded</div>
18
+ <div className="text-sm mt-2">
19
+ Waiting for environment to connect...
20
+ </div>
21
+ </div>
22
+ </div>
23
+ )
24
+ }
25
+
26
+ // Compact mode: simple list for sidebar
27
+ if (compact) {
28
+ return (
29
+ <div className="p-2 space-y-1">
30
+ {promptObjects.map((po) => (
31
+ <CompactPOItem key={po.name} po={po} />
32
+ ))}
33
+ </div>
34
+ )
35
+ }
36
+
37
+ // Full dashboard view
38
+ return (
39
+ <div className="h-full overflow-auto p-6">
40
+ <h1 className="text-2xl font-semibold text-white mb-6">
41
+ Prompt Objects
42
+ </h1>
43
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
44
+ {promptObjects.map((po) => (
45
+ <POCard key={po.name} po={po} />
46
+ ))}
47
+ </div>
48
+ </div>
49
+ )
50
+ }
51
+
52
+ function CompactPOItem({ po }: { po: PromptObject }) {
53
+ const { selectPO, selectedPO } = useStore()
54
+ const notifications = usePONotifications(po.name)
55
+ const isSelected = selectedPO === po.name
56
+
57
+ const statusColors = {
58
+ idle: 'bg-gray-500',
59
+ thinking: 'bg-po-accent animate-pulse',
60
+ calling_tool: 'bg-po-warning animate-pulse',
61
+ }
62
+
63
+ return (
64
+ <button
65
+ onClick={() => selectPO(po.name)}
66
+ className={`w-full text-left px-3 py-2 rounded transition-colors flex items-center gap-2 ${
67
+ isSelected
68
+ ? 'bg-po-accent/20 border border-po-accent'
69
+ : 'hover:bg-po-bg border border-transparent'
70
+ }`}
71
+ >
72
+ <div className={`w-2 h-2 rounded-full flex-shrink-0 ${statusColors[po.status]}`} />
73
+ <span className={`flex-1 truncate text-sm ${isSelected ? 'text-white' : 'text-gray-300'}`}>
74
+ {po.name}
75
+ </span>
76
+ {notifications.length > 0 && (
77
+ <span className="bg-po-warning text-black text-xs font-bold px-1.5 py-0.5 rounded-full">
78
+ {notifications.length}
79
+ </span>
80
+ )}
81
+ </button>
82
+ )
83
+ }
@@ -0,0 +1,141 @@
1
+ import { useState, useEffect, useRef } from 'react'
2
+ import { useStore, useNotificationCount } from '../store'
3
+ import { ModelSelector } from './ModelSelector'
4
+
5
+ interface Props {
6
+ switchLLM: (provider: string, model?: string) => void
7
+ }
8
+
9
+ export function Header({ switchLLM }: Props) {
10
+ const { connected, environment, selectedPO, selectPO, toggleBus, busOpen, notifications } =
11
+ useStore()
12
+ const notificationCount = useNotificationCount()
13
+ const [showNotifications, setShowNotifications] = useState(false)
14
+ const [animate, setAnimate] = useState(false)
15
+ const prevCount = useRef(notificationCount)
16
+
17
+ // Animate badge when count increases
18
+ useEffect(() => {
19
+ if (notificationCount > prevCount.current) {
20
+ setAnimate(true)
21
+ const timer = setTimeout(() => setAnimate(false), 500)
22
+ return () => clearTimeout(timer)
23
+ }
24
+ prevCount.current = notificationCount
25
+ }, [notificationCount])
26
+
27
+ return (
28
+ <header className="h-14 bg-po-surface border-b border-po-border flex items-center px-4 gap-4">
29
+ {/* Logo / Title */}
30
+ <button
31
+ onClick={() => selectPO(null)}
32
+ className="text-lg font-semibold text-white hover:text-po-accent transition-colors"
33
+ >
34
+ PromptObjects
35
+ </button>
36
+
37
+ {/* Environment info */}
38
+ {environment && (
39
+ <div className="text-sm text-gray-400">
40
+ <span className="text-gray-500">/</span>
41
+ <span className="ml-2">{environment.name}</span>
42
+ <span className="ml-3 text-gray-500">
43
+ {environment.po_count} POs, {environment.primitive_count} primitives
44
+ </span>
45
+ </div>
46
+ )}
47
+
48
+ {/* Breadcrumb for selected PO */}
49
+ {selectedPO && (
50
+ <div className="text-sm text-gray-400">
51
+ <span className="text-gray-500">/</span>
52
+ <span className="ml-2 text-po-accent">{selectedPO}</span>
53
+ </div>
54
+ )}
55
+
56
+ <div className="flex-1" />
57
+
58
+ {/* Model selector */}
59
+ <ModelSelector switchLLM={switchLLM} />
60
+
61
+ {/* Connection status */}
62
+ <div className="flex items-center gap-2 text-sm">
63
+ <div
64
+ className={`w-2 h-2 rounded-full ${
65
+ connected ? 'bg-green-500' : 'bg-red-500'
66
+ }`}
67
+ />
68
+ <span className="text-gray-400">
69
+ {connected ? 'Connected' : 'Disconnected'}
70
+ </span>
71
+ </div>
72
+
73
+ {/* Notification bell with badge */}
74
+ <div className="relative">
75
+ <button
76
+ onClick={() => setShowNotifications(!showNotifications)}
77
+ className={`relative p-2 rounded transition-colors ${
78
+ notificationCount > 0
79
+ ? 'text-po-warning hover:bg-po-warning/20'
80
+ : 'text-gray-400 hover:text-white hover:bg-po-border'
81
+ }`}
82
+ title={notificationCount > 0 ? `${notificationCount} pending requests` : 'No notifications'}
83
+ >
84
+ {/* Bell icon */}
85
+ <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
86
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9" />
87
+ </svg>
88
+ {/* Badge */}
89
+ {notificationCount > 0 && (
90
+ <span
91
+ className={`absolute -top-1 -right-1 bg-po-warning text-black text-xs font-bold w-5 h-5 flex items-center justify-center rounded-full ${
92
+ animate ? 'animate-bounce' : ''
93
+ }`}
94
+ >
95
+ {notificationCount}
96
+ </span>
97
+ )}
98
+ </button>
99
+
100
+ {/* Dropdown with notification summaries */}
101
+ {showNotifications && notifications.length > 0 && (
102
+ <div className="absolute right-0 top-full mt-2 w-80 bg-po-surface border border-po-border rounded-lg shadow-xl z-50">
103
+ <div className="p-3 border-b border-po-border">
104
+ <h3 className="font-medium text-white">Pending Requests</h3>
105
+ </div>
106
+ <div className="max-h-64 overflow-auto">
107
+ {notifications.map((n) => (
108
+ <div key={n.id} className="p-3 border-b border-po-border last:border-0 hover:bg-po-bg">
109
+ <div className="flex items-center gap-2 mb-1">
110
+ <span className="text-xs bg-po-warning text-black px-1.5 py-0.5 rounded font-medium">
111
+ {n.type}
112
+ </span>
113
+ <span className="text-xs text-po-accent">{n.po_name}</span>
114
+ </div>
115
+ <p className="text-sm text-gray-300 line-clamp-2">{n.message}</p>
116
+ </div>
117
+ ))}
118
+ </div>
119
+ <div className="p-2 border-t border-po-border">
120
+ <p className="text-xs text-gray-500 text-center">
121
+ Respond in the notification panel below
122
+ </p>
123
+ </div>
124
+ </div>
125
+ )}
126
+ </div>
127
+
128
+ {/* Message Bus toggle */}
129
+ <button
130
+ onClick={toggleBus}
131
+ className={`px-3 py-1.5 text-sm rounded transition-colors ${
132
+ busOpen
133
+ ? 'bg-po-accent text-white'
134
+ : 'bg-po-border text-gray-300 hover:bg-po-accent/50'
135
+ }`}
136
+ >
137
+ Bus
138
+ </button>
139
+ </header>
140
+ )
141
+ }