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,153 @@
|
|
|
1
|
+
import ReactMarkdown from 'react-markdown'
|
|
2
|
+
import remarkGfm from 'remark-gfm'
|
|
3
|
+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
|
|
4
|
+
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
|
|
7
|
+
interface MarkdownMessageProps {
|
|
8
|
+
content: string
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function MarkdownMessage({ content, className = '' }: MarkdownMessageProps) {
|
|
13
|
+
return (
|
|
14
|
+
<div className={`markdown-content ${className}`}>
|
|
15
|
+
<ReactMarkdown
|
|
16
|
+
remarkPlugins={[remarkGfm]}
|
|
17
|
+
components={{
|
|
18
|
+
// Code blocks with syntax highlighting
|
|
19
|
+
code({ className, children, ...props }) {
|
|
20
|
+
const match = /language-(\w+)/.exec(className || '')
|
|
21
|
+
const language = match ? match[1] : ''
|
|
22
|
+
const codeString = String(children).replace(/\n$/, '')
|
|
23
|
+
const isBlock = codeString.includes('\n') || match
|
|
24
|
+
|
|
25
|
+
if (isBlock) {
|
|
26
|
+
return (
|
|
27
|
+
<CodeBlock language={language} code={codeString} />
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<code
|
|
33
|
+
className="bg-po-bg px-1.5 py-0.5 rounded text-po-accent font-mono text-sm"
|
|
34
|
+
{...props}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</code>
|
|
38
|
+
)
|
|
39
|
+
},
|
|
40
|
+
// Styled links
|
|
41
|
+
a({ href, children }) {
|
|
42
|
+
return (
|
|
43
|
+
<a
|
|
44
|
+
href={href}
|
|
45
|
+
target="_blank"
|
|
46
|
+
rel="noopener noreferrer"
|
|
47
|
+
className="text-po-accent hover:underline"
|
|
48
|
+
>
|
|
49
|
+
{children}
|
|
50
|
+
</a>
|
|
51
|
+
)
|
|
52
|
+
},
|
|
53
|
+
// Styled paragraphs
|
|
54
|
+
p({ children }) {
|
|
55
|
+
return <p className="mb-3 last:mb-0">{children}</p>
|
|
56
|
+
},
|
|
57
|
+
// Styled lists
|
|
58
|
+
ul({ children }) {
|
|
59
|
+
return <ul className="list-disc list-inside mb-3 space-y-1">{children}</ul>
|
|
60
|
+
},
|
|
61
|
+
ol({ children }) {
|
|
62
|
+
return <ol className="list-decimal list-inside mb-3 space-y-1">{children}</ol>
|
|
63
|
+
},
|
|
64
|
+
// Styled headings
|
|
65
|
+
h1({ children }) {
|
|
66
|
+
return <h1 className="text-xl font-bold mb-2 mt-4 first:mt-0">{children}</h1>
|
|
67
|
+
},
|
|
68
|
+
h2({ children }) {
|
|
69
|
+
return <h2 className="text-lg font-bold mb-2 mt-3 first:mt-0">{children}</h2>
|
|
70
|
+
},
|
|
71
|
+
h3({ children }) {
|
|
72
|
+
return <h3 className="text-base font-bold mb-2 mt-2 first:mt-0">{children}</h3>
|
|
73
|
+
},
|
|
74
|
+
// Styled blockquotes
|
|
75
|
+
blockquote({ children }) {
|
|
76
|
+
return (
|
|
77
|
+
<blockquote className="border-l-4 border-po-accent pl-4 my-3 text-gray-400 italic">
|
|
78
|
+
{children}
|
|
79
|
+
</blockquote>
|
|
80
|
+
)
|
|
81
|
+
},
|
|
82
|
+
// Styled tables
|
|
83
|
+
table({ children }) {
|
|
84
|
+
return (
|
|
85
|
+
<div className="overflow-x-auto my-3">
|
|
86
|
+
<table className="min-w-full border border-po-border">{children}</table>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
th({ children }) {
|
|
91
|
+
return (
|
|
92
|
+
<th className="border border-po-border bg-po-bg px-3 py-2 text-left font-medium">
|
|
93
|
+
{children}
|
|
94
|
+
</th>
|
|
95
|
+
)
|
|
96
|
+
},
|
|
97
|
+
td({ children }) {
|
|
98
|
+
return (
|
|
99
|
+
<td className="border border-po-border px-3 py-2">{children}</td>
|
|
100
|
+
)
|
|
101
|
+
},
|
|
102
|
+
// Horizontal rule
|
|
103
|
+
hr() {
|
|
104
|
+
return <hr className="border-po-border my-4" />
|
|
105
|
+
},
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
{content}
|
|
109
|
+
</ReactMarkdown>
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function CodeBlock({ language, code }: { language: string; code: string }) {
|
|
115
|
+
const [copied, setCopied] = useState(false)
|
|
116
|
+
|
|
117
|
+
const handleCopy = async () => {
|
|
118
|
+
await navigator.clipboard.writeText(code)
|
|
119
|
+
setCopied(true)
|
|
120
|
+
setTimeout(() => setCopied(false), 2000)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="relative group my-3">
|
|
125
|
+
{/* Language label and copy button */}
|
|
126
|
+
<div className="flex items-center justify-between bg-po-bg/80 px-3 py-1 rounded-t border border-b-0 border-po-border">
|
|
127
|
+
<span className="text-xs text-gray-500 font-mono">
|
|
128
|
+
{language || 'text'}
|
|
129
|
+
</span>
|
|
130
|
+
<button
|
|
131
|
+
onClick={handleCopy}
|
|
132
|
+
className="text-xs text-gray-400 hover:text-white transition-colors"
|
|
133
|
+
>
|
|
134
|
+
{copied ? 'Copied!' : 'Copy'}
|
|
135
|
+
</button>
|
|
136
|
+
</div>
|
|
137
|
+
{/* Code with syntax highlighting */}
|
|
138
|
+
<SyntaxHighlighter
|
|
139
|
+
style={oneDark}
|
|
140
|
+
language={language || 'text'}
|
|
141
|
+
PreTag="div"
|
|
142
|
+
customStyle={{
|
|
143
|
+
margin: 0,
|
|
144
|
+
borderRadius: '0 0 0.375rem 0.375rem',
|
|
145
|
+
border: '1px solid rgb(55, 65, 81)',
|
|
146
|
+
borderTop: 'none',
|
|
147
|
+
}}
|
|
148
|
+
>
|
|
149
|
+
{code}
|
|
150
|
+
</SyntaxHighlighter>
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
{msg.content.length > 200
|
|
45
|
+
? msg.content.slice(0, 200) + '...'
|
|
46
|
+
: msg.content}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
))
|
|
50
|
+
)}
|
|
51
|
+
<div ref={messagesEndRef} />
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from 'react'
|
|
2
|
+
import { useStore } from '../store'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
switchLLM: (provider: string, model?: string) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function ModelSelector({ switchLLM }: Props) {
|
|
9
|
+
const { llmConfig } = useStore()
|
|
10
|
+
const [isOpen, setIsOpen] = useState(false)
|
|
11
|
+
const dropdownRef = useRef<HTMLDivElement>(null)
|
|
12
|
+
|
|
13
|
+
// Close dropdown when clicking outside
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
function handleClickOutside(event: MouseEvent) {
|
|
16
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
17
|
+
setIsOpen(false)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
21
|
+
return () => document.removeEventListener('mousedown', handleClickOutside)
|
|
22
|
+
}, [])
|
|
23
|
+
|
|
24
|
+
if (!llmConfig) return null
|
|
25
|
+
|
|
26
|
+
const handleSelectModel = (provider: string, model: string) => {
|
|
27
|
+
switchLLM(provider, model)
|
|
28
|
+
setIsOpen(false)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Provider display names
|
|
32
|
+
const providerNames: Record<string, string> = {
|
|
33
|
+
openai: 'OpenAI',
|
|
34
|
+
anthropic: 'Anthropic',
|
|
35
|
+
gemini: 'Gemini',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="relative" ref={dropdownRef}>
|
|
40
|
+
<button
|
|
41
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
42
|
+
className="flex items-center gap-2 px-3 py-1.5 text-sm bg-po-border rounded hover:bg-po-accent/50 transition-colors"
|
|
43
|
+
>
|
|
44
|
+
<span className="text-gray-400">
|
|
45
|
+
{providerNames[llmConfig.current_provider] || llmConfig.current_provider}
|
|
46
|
+
</span>
|
|
47
|
+
<span className="text-white font-medium">{llmConfig.current_model}</span>
|
|
48
|
+
<svg
|
|
49
|
+
className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
|
50
|
+
fill="none"
|
|
51
|
+
stroke="currentColor"
|
|
52
|
+
viewBox="0 0 24 24"
|
|
53
|
+
>
|
|
54
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
|
55
|
+
</svg>
|
|
56
|
+
</button>
|
|
57
|
+
|
|
58
|
+
{isOpen && (
|
|
59
|
+
<div className="absolute right-0 top-full mt-2 w-64 bg-po-surface border border-po-border rounded-lg shadow-xl z-50 overflow-hidden">
|
|
60
|
+
{llmConfig.providers.map((provider) => (
|
|
61
|
+
<div key={provider.name}>
|
|
62
|
+
<div className="px-3 py-2 bg-po-bg text-xs font-medium text-gray-400 uppercase tracking-wide flex items-center justify-between">
|
|
63
|
+
<span>{providerNames[provider.name] || provider.name}</span>
|
|
64
|
+
{!provider.available && (
|
|
65
|
+
<span className="text-red-400 text-[10px] normal-case">No API Key</span>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
{provider.models.map((model) => {
|
|
69
|
+
const isSelected =
|
|
70
|
+
provider.name === llmConfig.current_provider &&
|
|
71
|
+
model === llmConfig.current_model
|
|
72
|
+
const isAvailable = provider.available
|
|
73
|
+
const isDefault = model === provider.default_model
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<button
|
|
77
|
+
key={`${provider.name}-${model}`}
|
|
78
|
+
onClick={() => isAvailable && handleSelectModel(provider.name, model)}
|
|
79
|
+
disabled={!isAvailable}
|
|
80
|
+
className={`w-full px-3 py-2 text-left text-sm flex items-center justify-between transition-colors ${
|
|
81
|
+
isSelected
|
|
82
|
+
? 'bg-po-accent/20 text-po-accent'
|
|
83
|
+
: isAvailable
|
|
84
|
+
? 'text-gray-300 hover:bg-po-border'
|
|
85
|
+
: 'text-gray-600 cursor-not-allowed'
|
|
86
|
+
}`}
|
|
87
|
+
>
|
|
88
|
+
<span className="flex items-center gap-2">
|
|
89
|
+
{model}
|
|
90
|
+
{isDefault && (
|
|
91
|
+
<span className="text-[10px] text-gray-500">(default)</span>
|
|
92
|
+
)}
|
|
93
|
+
</span>
|
|
94
|
+
{isSelected && (
|
|
95
|
+
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
|
96
|
+
<path
|
|
97
|
+
fillRule="evenodd"
|
|
98
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
99
|
+
clipRule="evenodd"
|
|
100
|
+
/>
|
|
101
|
+
</svg>
|
|
102
|
+
)}
|
|
103
|
+
</button>
|
|
104
|
+
)
|
|
105
|
+
})}
|
|
106
|
+
</div>
|
|
107
|
+
))}
|
|
108
|
+
</div>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useStore } from '../store'
|
|
3
|
+
|
|
4
|
+
interface NotificationPanelProps {
|
|
5
|
+
respondToNotification: (id: string, response: string) => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function NotificationPanel({
|
|
9
|
+
respondToNotification,
|
|
10
|
+
}: NotificationPanelProps) {
|
|
11
|
+
const { notifications, selectPO } = useStore()
|
|
12
|
+
|
|
13
|
+
if (notifications.length === 0) return null
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className="fixed bottom-4 right-4 w-96 max-h-[60vh] overflow-auto bg-po-surface border border-po-border rounded-lg shadow-xl">
|
|
17
|
+
<div className="sticky top-0 bg-po-surface border-b border-po-border p-3">
|
|
18
|
+
<h3 className="font-medium text-white">
|
|
19
|
+
Notifications ({notifications.length})
|
|
20
|
+
</h3>
|
|
21
|
+
</div>
|
|
22
|
+
<div className="p-2 space-y-2">
|
|
23
|
+
{notifications.map((notification) => (
|
|
24
|
+
<NotificationCard
|
|
25
|
+
key={notification.id}
|
|
26
|
+
notification={notification}
|
|
27
|
+
onRespond={(response) =>
|
|
28
|
+
respondToNotification(notification.id, response)
|
|
29
|
+
}
|
|
30
|
+
onViewPO={() => selectPO(notification.po_name)}
|
|
31
|
+
/>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface NotificationCardProps {
|
|
39
|
+
notification: {
|
|
40
|
+
id: string
|
|
41
|
+
po_name: string
|
|
42
|
+
type: string
|
|
43
|
+
message: string
|
|
44
|
+
options: string[]
|
|
45
|
+
}
|
|
46
|
+
onRespond: (response: string) => void
|
|
47
|
+
onViewPO: () => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function NotificationCard({
|
|
51
|
+
notification,
|
|
52
|
+
onRespond,
|
|
53
|
+
onViewPO,
|
|
54
|
+
}: NotificationCardProps) {
|
|
55
|
+
const [customInput, setCustomInput] = useState('')
|
|
56
|
+
const [showCustom, setShowCustom] = useState(false)
|
|
57
|
+
|
|
58
|
+
const handleOptionClick = (option: string) => {
|
|
59
|
+
onRespond(option)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const handleCustomSubmit = () => {
|
|
63
|
+
if (customInput.trim()) {
|
|
64
|
+
onRespond(customInput.trim())
|
|
65
|
+
setCustomInput('')
|
|
66
|
+
setShowCustom(false)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className="bg-po-bg border border-po-border rounded-lg p-3">
|
|
72
|
+
<div className="flex items-center gap-2 mb-2">
|
|
73
|
+
<span className="text-xs bg-po-warning text-black px-2 py-0.5 rounded font-medium">
|
|
74
|
+
{notification.type}
|
|
75
|
+
</span>
|
|
76
|
+
<button
|
|
77
|
+
onClick={onViewPO}
|
|
78
|
+
className="text-xs text-po-accent hover:underline"
|
|
79
|
+
>
|
|
80
|
+
{notification.po_name}
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<p className="text-sm text-gray-200 mb-3">{notification.message}</p>
|
|
85
|
+
|
|
86
|
+
{notification.options.length > 0 && (
|
|
87
|
+
<div className="flex flex-wrap gap-2 mb-2">
|
|
88
|
+
{notification.options.map((option, index) => (
|
|
89
|
+
<button
|
|
90
|
+
key={index}
|
|
91
|
+
onClick={() => handleOptionClick(option)}
|
|
92
|
+
className="px-3 py-1.5 text-sm bg-po-surface border border-po-border rounded hover:border-po-accent transition-colors"
|
|
93
|
+
>
|
|
94
|
+
{option}
|
|
95
|
+
</button>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
)}
|
|
99
|
+
|
|
100
|
+
{showCustom ? (
|
|
101
|
+
<div className="flex gap-2">
|
|
102
|
+
<input
|
|
103
|
+
type="text"
|
|
104
|
+
value={customInput}
|
|
105
|
+
onChange={(e) => setCustomInput(e.target.value)}
|
|
106
|
+
placeholder="Custom response..."
|
|
107
|
+
className="flex-1 bg-po-surface border border-po-border rounded px-2 py-1 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-po-accent"
|
|
108
|
+
onKeyDown={(e) => e.key === 'Enter' && handleCustomSubmit()}
|
|
109
|
+
autoFocus
|
|
110
|
+
/>
|
|
111
|
+
<button
|
|
112
|
+
onClick={handleCustomSubmit}
|
|
113
|
+
className="px-2 py-1 text-sm bg-po-accent text-white rounded hover:bg-po-accent/80"
|
|
114
|
+
>
|
|
115
|
+
Send
|
|
116
|
+
</button>
|
|
117
|
+
<button
|
|
118
|
+
onClick={() => setShowCustom(false)}
|
|
119
|
+
className="px-2 py-1 text-sm text-gray-400 hover:text-white"
|
|
120
|
+
>
|
|
121
|
+
Cancel
|
|
122
|
+
</button>
|
|
123
|
+
</div>
|
|
124
|
+
) : (
|
|
125
|
+
<button
|
|
126
|
+
onClick={() => setShowCustom(true)}
|
|
127
|
+
className="text-xs text-gray-400 hover:text-white"
|
|
128
|
+
>
|
|
129
|
+
+ Custom response
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
)
|
|
134
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
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
|
+
}
|
|
13
|
+
|
|
14
|
+
export function PODetail({
|
|
15
|
+
sendMessage,
|
|
16
|
+
createSession,
|
|
17
|
+
switchSession,
|
|
18
|
+
createThread,
|
|
19
|
+
}: PODetailProps) {
|
|
20
|
+
const { activeTab, setActiveTab, selectPO } = useStore()
|
|
21
|
+
const po = useSelectedPO()
|
|
22
|
+
const notifications = usePONotifications(po?.name || '')
|
|
23
|
+
|
|
24
|
+
if (!po) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="h-full flex items-center justify-center text-gray-500">
|
|
27
|
+
Select a Prompt Object
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const tabs = [
|
|
33
|
+
{ id: 'chat' as const, label: 'Chat' },
|
|
34
|
+
{ id: 'sessions' as const, label: `Threads (${po.sessions?.length || 0})` },
|
|
35
|
+
{ id: 'capabilities' as const, label: `Capabilities (${po.capabilities?.length || 0})` },
|
|
36
|
+
{ id: 'prompt' as const, label: 'Prompt' },
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="h-full flex flex-col">
|
|
41
|
+
{/* PO Header */}
|
|
42
|
+
<div className="border-b border-po-border bg-po-surface px-4 py-3">
|
|
43
|
+
<div className="flex items-center justify-between mb-2">
|
|
44
|
+
<div className="flex items-center gap-3">
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => selectPO(null)}
|
|
47
|
+
className="text-gray-400 hover:text-white transition-colors"
|
|
48
|
+
>
|
|
49
|
+
← Back
|
|
50
|
+
</button>
|
|
51
|
+
<h2 className="text-lg font-semibold text-white">{po.name}</h2>
|
|
52
|
+
{notifications.length > 0 && (
|
|
53
|
+
<span className="bg-po-warning text-black text-xs font-bold px-2 py-0.5 rounded-full">
|
|
54
|
+
{notifications.length} pending
|
|
55
|
+
</span>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
<div className="flex items-center gap-2 text-sm">
|
|
59
|
+
<StatusIndicator status={po.status} />
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
<p className="text-sm text-gray-400">{po.description}</p>
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{/* Tabs */}
|
|
66
|
+
<div className="border-b border-po-border bg-po-surface px-4">
|
|
67
|
+
<div className="flex gap-1">
|
|
68
|
+
{tabs.map((tab) => (
|
|
69
|
+
<button
|
|
70
|
+
key={tab.id}
|
|
71
|
+
onClick={() => setActiveTab(tab.id)}
|
|
72
|
+
className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
|
|
73
|
+
activeTab === tab.id
|
|
74
|
+
? 'text-po-accent border-po-accent'
|
|
75
|
+
: 'text-gray-400 border-transparent hover:text-white'
|
|
76
|
+
}`}
|
|
77
|
+
>
|
|
78
|
+
{tab.label}
|
|
79
|
+
</button>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
{/* Tab content */}
|
|
85
|
+
<div className="flex-1 overflow-hidden">
|
|
86
|
+
{activeTab === 'chat' && (
|
|
87
|
+
<ChatPanel po={po} sendMessage={sendMessage} />
|
|
88
|
+
)}
|
|
89
|
+
{activeTab === 'sessions' && (
|
|
90
|
+
<SessionsPanel
|
|
91
|
+
po={po}
|
|
92
|
+
createSession={createSession}
|
|
93
|
+
switchSession={switchSession}
|
|
94
|
+
createThread={createThread}
|
|
95
|
+
/>
|
|
96
|
+
)}
|
|
97
|
+
{activeTab === 'capabilities' && <CapabilitiesPanel po={po} />}
|
|
98
|
+
{activeTab === 'prompt' && <PromptPanel po={po} />}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function StatusIndicator({ status }: { status: string }) {
|
|
105
|
+
const config = {
|
|
106
|
+
idle: { color: 'bg-gray-500', label: 'Idle' },
|
|
107
|
+
thinking: { color: 'bg-po-accent animate-pulse', label: 'Thinking...' },
|
|
108
|
+
calling_tool: { color: 'bg-po-warning animate-pulse', label: 'Calling tool...' },
|
|
109
|
+
}[status] || { color: 'bg-gray-500', label: status }
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex items-center gap-2">
|
|
113
|
+
<div className={`w-2 h-2 rounded-full ${config.color}`} />
|
|
114
|
+
<span className="text-gray-400">{config.label}</span>
|
|
115
|
+
</div>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PromptObject } from '../types'
|
|
2
|
+
import { MarkdownMessage } from './MarkdownMessage'
|
|
3
|
+
|
|
4
|
+
interface PromptPanelProps {
|
|
5
|
+
po: PromptObject
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function PromptPanel({ po }: PromptPanelProps) {
|
|
9
|
+
const prompt = po.prompt || ''
|
|
10
|
+
const config = po.config || {}
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="h-full overflow-auto p-4">
|
|
14
|
+
{/* Config/Frontmatter */}
|
|
15
|
+
<div className="mb-6">
|
|
16
|
+
<h3 className="text-lg font-medium text-white mb-3">Configuration</h3>
|
|
17
|
+
<div className="bg-po-bg rounded-lg border border-po-border p-4">
|
|
18
|
+
<pre className="text-sm text-gray-300 font-mono whitespace-pre-wrap">
|
|
19
|
+
{JSON.stringify(config, null, 2)}
|
|
20
|
+
</pre>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
{/* Prompt/Body */}
|
|
25
|
+
<div>
|
|
26
|
+
<h3 className="text-lg font-medium text-white mb-3">Prompt</h3>
|
|
27
|
+
<div className="bg-po-bg rounded-lg border border-po-border p-4">
|
|
28
|
+
{prompt ? (
|
|
29
|
+
<div className="prose prose-invert max-w-none">
|
|
30
|
+
<MarkdownMessage content={prompt} />
|
|
31
|
+
</div>
|
|
32
|
+
) : (
|
|
33
|
+
<p className="text-gray-500 italic">No prompt defined</p>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
{/* Raw source toggle */}
|
|
39
|
+
<details className="mt-6">
|
|
40
|
+
<summary className="text-sm text-gray-400 cursor-pointer hover:text-white">
|
|
41
|
+
View raw source
|
|
42
|
+
</summary>
|
|
43
|
+
<div className="mt-2 bg-po-bg rounded-lg border border-po-border p-4">
|
|
44
|
+
<pre className="text-xs text-gray-400 font-mono whitespace-pre-wrap overflow-x-auto">
|
|
45
|
+
{prompt || '(empty)'}
|
|
46
|
+
</pre>
|
|
47
|
+
</div>
|
|
48
|
+
</details>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|