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,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
+ }