prompt_objects 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/frontend/src/App.tsx +2 -1
- data/frontend/src/components/CapabilitiesPanel.tsx +122 -25
- data/frontend/src/components/ChatPanel.tsx +43 -6
- data/frontend/src/components/PODetail.tsx +8 -1
- data/frontend/src/components/PromptPanel.tsx +124 -19
- data/frontend/src/hooks/useWebSocket.ts +16 -0
- data/frontend/src/types/index.ts +12 -1
- data/lib/prompt_objects/environment.rb +11 -0
- data/lib/prompt_objects/prompt_object.rb +10 -0
- data/lib/prompt_objects/server/public/assets/index-CeNJvqLG.js +77 -0
- data/lib/prompt_objects/server/public/assets/index-Vx4-uMOU.css +1 -0
- data/lib/prompt_objects/server/public/index.html +2 -2
- data/lib/prompt_objects/server/websocket_handler.rb +67 -3
- data/lib/prompt_objects/server.rb +13 -0
- data/lib/prompt_objects/universal/add_capability.rb +6 -1
- data/lib/prompt_objects/universal/add_primitive.rb +6 -1
- data/lib/prompt_objects/universal/create_capability.rb +4 -0
- data/lib/prompt_objects/universal/create_primitive.rb +4 -0
- data/lib/prompt_objects/universal/delete_primitive.rb +77 -0
- data/lib/prompt_objects/universal/modify_prompt.rb +164 -0
- data/lib/prompt_objects/universal/remove_capability.rb +73 -0
- data/lib/prompt_objects.rb +4 -1
- data/prompt_objects.gemspec +1 -1
- metadata +6 -3
- data/lib/prompt_objects/server/public/assets/index-2acS2FYZ.js +0 -77
- data/lib/prompt_objects/server/public/assets/index-DXU5uRXQ.css +0 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0fe57111f23be7238465e5a386d48d8d694cd4599e2113e6378150951efcd303
|
|
4
|
+
data.tar.gz: '07192c4f47af2d7aa9530fdc61b65cb97fba77ae84133a30533d3f07314e0b79'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 66d2e26018f49685968bccfd075b2e7cf74fb4bd0cbdf0730f4f3c68c3101be232235beb86a8a1cde73bdf2707c1a679b916d53c8b4c1fb2f9d455d965e4d246
|
|
7
|
+
data.tar.gz: 5fb3e4b088aaf7a6a99e08e144f090c68e2a63b0b009fe43929d93daa8da7b9e6b0c330ae048c2d6bda9fbcdef385000c0f255695a539e922393fc3c84a59e84
|
data/frontend/src/App.tsx
CHANGED
|
@@ -9,7 +9,7 @@ import { NotificationPanel } from './components/NotificationPanel'
|
|
|
9
9
|
import { ThreadsSidebar } from './components/ThreadsSidebar'
|
|
10
10
|
|
|
11
11
|
export default function App() {
|
|
12
|
-
const { sendMessage, respondToNotification, createSession, switchSession, switchLLM, createThread } =
|
|
12
|
+
const { sendMessage, respondToNotification, createSession, switchSession, switchLLM, createThread, updatePrompt } =
|
|
13
13
|
useWebSocket()
|
|
14
14
|
const { selectedPO, busOpen, notifications } = useStore()
|
|
15
15
|
const selectedPOData = useSelectedPO()
|
|
@@ -72,6 +72,7 @@ export default function App() {
|
|
|
72
72
|
createSession={createSession}
|
|
73
73
|
switchSession={switchSession}
|
|
74
74
|
createThread={createThread}
|
|
75
|
+
updatePrompt={updatePrompt}
|
|
75
76
|
/>
|
|
76
77
|
) : (
|
|
77
78
|
<Dashboard />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import type { PromptObject, CapabilityInfo } from '../types'
|
|
2
3
|
|
|
3
4
|
interface CapabilitiesPanelProps {
|
|
4
5
|
po: PromptObject
|
|
@@ -6,38 +7,134 @@ interface CapabilitiesPanelProps {
|
|
|
6
7
|
|
|
7
8
|
export function CapabilitiesPanel({ po }: CapabilitiesPanelProps) {
|
|
8
9
|
const capabilities = po.capabilities || []
|
|
10
|
+
const universalCapabilities = po.universal_capabilities || []
|
|
9
11
|
|
|
10
12
|
return (
|
|
11
13
|
<div className="h-full overflow-auto p-4">
|
|
12
|
-
|
|
14
|
+
{/* Declared Capabilities */}
|
|
15
|
+
<div className="mb-6">
|
|
16
|
+
<h3 className="text-lg font-medium text-white mb-3">
|
|
17
|
+
Declared Capabilities
|
|
18
|
+
<span className="ml-2 text-sm text-gray-500">({capabilities.length})</span>
|
|
19
|
+
</h3>
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
{capabilities.length === 0 ? (
|
|
22
|
+
<div className="text-gray-500 text-sm py-4 px-3 bg-po-bg rounded-lg border border-po-border">
|
|
23
|
+
No capabilities declared. This PO can only use universal capabilities.
|
|
24
|
+
</div>
|
|
25
|
+
) : (
|
|
26
|
+
<div className="space-y-1">
|
|
27
|
+
{capabilities.map((cap) => (
|
|
28
|
+
<CapabilityItem key={cap.name} capability={cap} accent />
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
)}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
{/* Universal Capabilities */}
|
|
35
|
+
<div>
|
|
36
|
+
<h3 className="text-lg font-medium text-white mb-3">
|
|
37
|
+
Universal Capabilities
|
|
38
|
+
<span className="ml-2 text-sm text-gray-500">({universalCapabilities.length})</span>
|
|
39
|
+
</h3>
|
|
40
|
+
<p className="text-xs text-gray-500 mb-3">
|
|
41
|
+
Available to all Prompt Objects automatically.
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<div className="space-y-1">
|
|
45
|
+
{universalCapabilities.map((cap) => (
|
|
46
|
+
<CapabilityItem key={cap.name} capability={cap} />
|
|
27
47
|
))}
|
|
28
48
|
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface CapabilityItemProps {
|
|
55
|
+
capability: CapabilityInfo
|
|
56
|
+
accent?: boolean // Use accent color for name (for declared caps)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CapabilityItem({ capability, accent }: CapabilityItemProps) {
|
|
60
|
+
const [expanded, setExpanded] = useState(false)
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className="bg-po-bg border border-po-border rounded-lg overflow-hidden">
|
|
64
|
+
<button
|
|
65
|
+
onClick={() => setExpanded(!expanded)}
|
|
66
|
+
className="w-full px-3 py-2 flex items-center justify-between hover:bg-po-surface transition-colors"
|
|
67
|
+
>
|
|
68
|
+
<span className={`font-mono text-sm ${accent ? 'text-po-accent' : 'text-gray-300'}`}>
|
|
69
|
+
{capability.name}
|
|
70
|
+
</span>
|
|
71
|
+
<span className="text-gray-500 text-xs">{expanded ? '▼' : '▶'}</span>
|
|
72
|
+
</button>
|
|
73
|
+
{expanded && (
|
|
74
|
+
<div className="px-3 py-2 border-t border-po-border bg-po-surface space-y-3">
|
|
75
|
+
<p className="text-xs text-gray-400">{capability.description}</p>
|
|
76
|
+
|
|
77
|
+
{capability.parameters && (
|
|
78
|
+
<ParametersDisplay parameters={capability.parameters} />
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
29
81
|
)}
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
30
85
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
86
|
+
interface ParametersDisplayProps {
|
|
87
|
+
parameters: Record<string, unknown>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function ParametersDisplay({ parameters }: ParametersDisplayProps) {
|
|
91
|
+
const properties = (parameters.properties as Record<string, unknown>) || {}
|
|
92
|
+
const required = (parameters.required as string[]) || []
|
|
93
|
+
|
|
94
|
+
const propertyNames = Object.keys(properties)
|
|
95
|
+
|
|
96
|
+
if (propertyNames.length === 0) {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return (
|
|
101
|
+
<div>
|
|
102
|
+
<div className="text-xs text-gray-500 mb-2 font-medium">Parameters</div>
|
|
103
|
+
<div className="space-y-2">
|
|
104
|
+
{propertyNames.map((propName) => {
|
|
105
|
+
const prop = properties[propName] as Record<string, unknown>
|
|
106
|
+
const isRequired = required.includes(propName)
|
|
107
|
+
|
|
108
|
+
const propType = prop.type ? String(prop.type) : null
|
|
109
|
+
const propDescription = prop.description ? String(prop.description) : null
|
|
110
|
+
const propEnum = prop.enum as string[] | undefined
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div key={propName} className="bg-po-bg rounded p-2">
|
|
114
|
+
<div className="flex items-center gap-2">
|
|
115
|
+
<span className="font-mono text-xs text-po-accent">{propName}</span>
|
|
116
|
+
{propType && (
|
|
117
|
+
<span className="text-xs text-gray-600">({propType})</span>
|
|
118
|
+
)}
|
|
119
|
+
{isRequired && (
|
|
120
|
+
<span className="text-xs text-red-400">required</span>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
{propDescription && (
|
|
124
|
+
<p className="text-xs text-gray-500 mt-1">{propDescription}</p>
|
|
125
|
+
)}
|
|
126
|
+
{propEnum && propEnum.length > 0 && (
|
|
127
|
+
<div className="mt-1 flex flex-wrap gap-1">
|
|
128
|
+
{propEnum.map((val) => (
|
|
129
|
+
<span key={val} className="text-xs bg-po-surface px-1.5 py-0.5 rounded text-gray-400">
|
|
130
|
+
{val}
|
|
131
|
+
</span>
|
|
132
|
+
))}
|
|
133
|
+
</div>
|
|
134
|
+
)}
|
|
135
|
+
</div>
|
|
136
|
+
)
|
|
137
|
+
})}
|
|
41
138
|
</div>
|
|
42
139
|
</div>
|
|
43
140
|
)
|
|
@@ -133,13 +133,15 @@ function MessageBubble({ message }: { message: Message }) {
|
|
|
133
133
|
const isTool = message.role === 'tool'
|
|
134
134
|
|
|
135
135
|
if (isTool) {
|
|
136
|
+
// Tool messages contain results from tool calls
|
|
137
|
+
const results = message.results || []
|
|
138
|
+
if (results.length === 0) return null
|
|
139
|
+
|
|
136
140
|
return (
|
|
137
|
-
<div className="
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{message.content && message.content.length > 500 && '...'}
|
|
142
|
-
</div>
|
|
141
|
+
<div className="space-y-2 ml-11">
|
|
142
|
+
{results.map((result, idx) => (
|
|
143
|
+
<ToolResultDisplay key={result.tool_call_id || idx} result={result} />
|
|
144
|
+
))}
|
|
143
145
|
</div>
|
|
144
146
|
)
|
|
145
147
|
}
|
|
@@ -249,3 +251,38 @@ function ToolCallDisplay({ toolCall }: { toolCall: ToolCall }) {
|
|
|
249
251
|
</div>
|
|
250
252
|
)
|
|
251
253
|
}
|
|
254
|
+
|
|
255
|
+
function ToolResultDisplay({ result }: { result: { tool_call_id: string; name?: string; content: string } }) {
|
|
256
|
+
const [expanded, setExpanded] = useState(false)
|
|
257
|
+
const content = result.content || ''
|
|
258
|
+
const truncatedContent = content.slice(0, 200)
|
|
259
|
+
const isTruncated = content.length > 200
|
|
260
|
+
|
|
261
|
+
return (
|
|
262
|
+
<div className="text-xs bg-po-surface/50 border border-po-border/30 rounded overflow-hidden">
|
|
263
|
+
<button
|
|
264
|
+
onClick={() => setExpanded(!expanded)}
|
|
265
|
+
className="w-full px-2 py-1 text-left font-mono flex items-center gap-1 hover:bg-po-surface transition-colors"
|
|
266
|
+
>
|
|
267
|
+
<span className="text-gray-500">{expanded ? '▼' : '▶'}</span>
|
|
268
|
+
<span className="text-gray-400">↳ Result</span>
|
|
269
|
+
{result.name && <span className="text-po-accent/70">from {result.name}</span>}
|
|
270
|
+
</button>
|
|
271
|
+
{expanded && (
|
|
272
|
+
<div className="px-2 pb-2 border-t border-po-border/30">
|
|
273
|
+
<pre className="text-gray-400 whitespace-pre-wrap break-all mt-1 text-[10px] leading-relaxed">
|
|
274
|
+
{content}
|
|
275
|
+
</pre>
|
|
276
|
+
</div>
|
|
277
|
+
)}
|
|
278
|
+
{!expanded && (
|
|
279
|
+
<div className="px-2 pb-1 text-gray-500">
|
|
280
|
+
<span className="whitespace-pre-wrap break-all">
|
|
281
|
+
{truncatedContent}
|
|
282
|
+
{isTruncated && '...'}
|
|
283
|
+
</span>
|
|
284
|
+
</div>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
)
|
|
288
|
+
}
|
|
@@ -9,6 +9,7 @@ interface PODetailProps {
|
|
|
9
9
|
createSession: (target: string, name?: string) => void
|
|
10
10
|
switchSession: (target: string, sessionId: string) => void
|
|
11
11
|
createThread: (target: string, name?: string) => void
|
|
12
|
+
updatePrompt: (target: string, prompt: string) => void
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function PODetail({
|
|
@@ -16,6 +17,7 @@ export function PODetail({
|
|
|
16
17
|
createSession,
|
|
17
18
|
switchSession,
|
|
18
19
|
createThread,
|
|
20
|
+
updatePrompt,
|
|
19
21
|
}: PODetailProps) {
|
|
20
22
|
const { activeTab, setActiveTab, selectPO } = useStore()
|
|
21
23
|
const po = useSelectedPO()
|
|
@@ -95,7 +97,12 @@ export function PODetail({
|
|
|
95
97
|
/>
|
|
96
98
|
)}
|
|
97
99
|
{activeTab === 'capabilities' && <CapabilitiesPanel po={po} />}
|
|
98
|
-
{activeTab === 'prompt' &&
|
|
100
|
+
{activeTab === 'prompt' && (
|
|
101
|
+
<PromptPanel
|
|
102
|
+
po={po}
|
|
103
|
+
onSave={(prompt) => updatePrompt(po.name, prompt)}
|
|
104
|
+
/>
|
|
105
|
+
)}
|
|
99
106
|
</div>
|
|
100
107
|
</div>
|
|
101
108
|
)
|
|
@@ -1,13 +1,78 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from 'react'
|
|
1
2
|
import type { PromptObject } from '../types'
|
|
2
3
|
import { MarkdownMessage } from './MarkdownMessage'
|
|
3
4
|
|
|
4
5
|
interface PromptPanelProps {
|
|
5
6
|
po: PromptObject
|
|
7
|
+
onSave?: (prompt: string) => void
|
|
6
8
|
}
|
|
7
9
|
|
|
8
|
-
export function PromptPanel({ po }: PromptPanelProps) {
|
|
10
|
+
export function PromptPanel({ po, onSave }: PromptPanelProps) {
|
|
9
11
|
const prompt = po.prompt || ''
|
|
10
12
|
const config = po.config || {}
|
|
13
|
+
const [isEditing, setIsEditing] = useState(false)
|
|
14
|
+
const [editedPrompt, setEditedPrompt] = useState(prompt)
|
|
15
|
+
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved')
|
|
16
|
+
const saveTimeoutRef = useRef<number | null>(null)
|
|
17
|
+
|
|
18
|
+
// Sync editedPrompt when po.prompt changes from server
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!isEditing) {
|
|
21
|
+
setEditedPrompt(prompt)
|
|
22
|
+
}
|
|
23
|
+
}, [prompt, isEditing])
|
|
24
|
+
|
|
25
|
+
// Debounced auto-save
|
|
26
|
+
const debouncedSave = useCallback((newPrompt: string) => {
|
|
27
|
+
if (saveTimeoutRef.current) {
|
|
28
|
+
clearTimeout(saveTimeoutRef.current)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
setSaveStatus('unsaved')
|
|
32
|
+
|
|
33
|
+
saveTimeoutRef.current = window.setTimeout(() => {
|
|
34
|
+
if (onSave && newPrompt !== prompt) {
|
|
35
|
+
setSaveStatus('saving')
|
|
36
|
+
onSave(newPrompt)
|
|
37
|
+
// Assume save succeeded - server will broadcast update
|
|
38
|
+
setTimeout(() => setSaveStatus('saved'), 500)
|
|
39
|
+
} else {
|
|
40
|
+
setSaveStatus('saved')
|
|
41
|
+
}
|
|
42
|
+
}, 1000) // 1 second debounce
|
|
43
|
+
}, [onSave, prompt])
|
|
44
|
+
|
|
45
|
+
const handlePromptChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
46
|
+
const newPrompt = e.target.value
|
|
47
|
+
setEditedPrompt(newPrompt)
|
|
48
|
+
debouncedSave(newPrompt)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const handleStartEditing = () => {
|
|
52
|
+
setEditedPrompt(prompt)
|
|
53
|
+
setIsEditing(true)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const handleStopEditing = () => {
|
|
57
|
+
// Save any pending changes
|
|
58
|
+
if (saveTimeoutRef.current) {
|
|
59
|
+
clearTimeout(saveTimeoutRef.current)
|
|
60
|
+
}
|
|
61
|
+
if (editedPrompt !== prompt && onSave) {
|
|
62
|
+
onSave(editedPrompt)
|
|
63
|
+
}
|
|
64
|
+
setIsEditing(false)
|
|
65
|
+
setSaveStatus('saved')
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Cleanup timeout on unmount
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
return () => {
|
|
71
|
+
if (saveTimeoutRef.current) {
|
|
72
|
+
clearTimeout(saveTimeoutRef.current)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}, [])
|
|
11
76
|
|
|
12
77
|
return (
|
|
13
78
|
<div className="h-full overflow-auto p-4">
|
|
@@ -23,29 +88,69 @@ export function PromptPanel({ po }: PromptPanelProps) {
|
|
|
23
88
|
|
|
24
89
|
{/* Prompt/Body */}
|
|
25
90
|
<div>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
91
|
+
<div className="flex items-center justify-between mb-3">
|
|
92
|
+
<h3 className="text-lg font-medium text-white">Prompt</h3>
|
|
93
|
+
<div className="flex items-center gap-3">
|
|
94
|
+
{isEditing && (
|
|
95
|
+
<span className={`text-xs ${
|
|
96
|
+
saveStatus === 'saved' ? 'text-green-400' :
|
|
97
|
+
saveStatus === 'saving' ? 'text-yellow-400' :
|
|
98
|
+
'text-gray-400'
|
|
99
|
+
}`}>
|
|
100
|
+
{saveStatus === 'saved' ? 'Saved' :
|
|
101
|
+
saveStatus === 'saving' ? 'Saving...' :
|
|
102
|
+
'Unsaved changes'}
|
|
103
|
+
</span>
|
|
104
|
+
)}
|
|
105
|
+
<button
|
|
106
|
+
onClick={isEditing ? handleStopEditing : handleStartEditing}
|
|
107
|
+
className={`px-3 py-1 text-sm rounded transition-colors ${
|
|
108
|
+
isEditing
|
|
109
|
+
? 'bg-po-accent text-black hover:bg-po-accent/80'
|
|
110
|
+
: 'bg-po-surface border border-po-border text-gray-300 hover:text-white hover:border-po-accent'
|
|
111
|
+
}`}
|
|
112
|
+
>
|
|
113
|
+
{isEditing ? 'Done' : 'Edit'}
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
<div className="bg-po-bg rounded-lg border border-po-border">
|
|
119
|
+
{isEditing ? (
|
|
120
|
+
<textarea
|
|
121
|
+
value={editedPrompt}
|
|
122
|
+
onChange={handlePromptChange}
|
|
123
|
+
className="w-full h-96 p-4 bg-transparent text-gray-200 font-mono text-sm resize-none focus:outline-none focus:ring-1 focus:ring-po-accent rounded-lg"
|
|
124
|
+
placeholder="Enter prompt markdown..."
|
|
125
|
+
spellCheck={false}
|
|
126
|
+
/>
|
|
32
127
|
) : (
|
|
33
|
-
<
|
|
128
|
+
<div className="p-4">
|
|
129
|
+
{prompt ? (
|
|
130
|
+
<div className="prose prose-invert max-w-none">
|
|
131
|
+
<MarkdownMessage content={prompt} />
|
|
132
|
+
</div>
|
|
133
|
+
) : (
|
|
134
|
+
<p className="text-gray-500 italic">No prompt defined. Click Edit to add one.</p>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
34
137
|
)}
|
|
35
138
|
</div>
|
|
36
139
|
</div>
|
|
37
140
|
|
|
38
|
-
{/* Raw source toggle */}
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
141
|
+
{/* Raw source toggle (only in view mode) */}
|
|
142
|
+
{!isEditing && (
|
|
143
|
+
<details className="mt-6">
|
|
144
|
+
<summary className="text-sm text-gray-400 cursor-pointer hover:text-white">
|
|
145
|
+
View raw source
|
|
146
|
+
</summary>
|
|
147
|
+
<div className="mt-2 bg-po-bg rounded-lg border border-po-border p-4">
|
|
148
|
+
<pre className="text-xs text-gray-400 font-mono whitespace-pre-wrap overflow-x-auto">
|
|
149
|
+
{prompt || '(empty)'}
|
|
150
|
+
</pre>
|
|
151
|
+
</div>
|
|
152
|
+
</details>
|
|
153
|
+
)}
|
|
49
154
|
</div>
|
|
50
155
|
)
|
|
51
156
|
}
|
|
@@ -352,6 +352,21 @@ export function useWebSocket() {
|
|
|
352
352
|
)
|
|
353
353
|
}, [])
|
|
354
354
|
|
|
355
|
+
// Update a PO's prompt (markdown body)
|
|
356
|
+
const updatePrompt = useCallback((target: string, prompt: string) => {
|
|
357
|
+
if (!ws.current || ws.current.readyState !== WebSocket.OPEN) {
|
|
358
|
+
console.error('WebSocket not connected')
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
ws.current.send(
|
|
363
|
+
JSON.stringify({
|
|
364
|
+
type: 'update_prompt',
|
|
365
|
+
payload: { target, prompt },
|
|
366
|
+
})
|
|
367
|
+
)
|
|
368
|
+
}, [])
|
|
369
|
+
|
|
355
370
|
return {
|
|
356
371
|
sendMessage,
|
|
357
372
|
respondToNotification,
|
|
@@ -359,5 +374,6 @@ export function useWebSocket() {
|
|
|
359
374
|
switchSession,
|
|
360
375
|
switchLLM,
|
|
361
376
|
createThread,
|
|
377
|
+
updatePrompt,
|
|
362
378
|
}
|
|
363
379
|
}
|
data/frontend/src/types/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface ToolCall {
|
|
|
16
16
|
|
|
17
17
|
export interface ToolResult {
|
|
18
18
|
tool_call_id: string
|
|
19
|
+
name?: string // Name of the tool that was called
|
|
19
20
|
content: string
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -42,11 +43,21 @@ export interface CurrentSession {
|
|
|
42
43
|
messages: Message[]
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export interface CapabilityInfo {
|
|
47
|
+
name: string
|
|
48
|
+
description: string
|
|
49
|
+
parameters?: Record<string, unknown>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Alias for backwards compatibility
|
|
53
|
+
export type UniversalCapability = CapabilityInfo
|
|
54
|
+
|
|
45
55
|
export interface PromptObject {
|
|
46
56
|
name: string
|
|
47
57
|
description: string
|
|
48
58
|
status: 'idle' | 'thinking' | 'calling_tool'
|
|
49
|
-
capabilities:
|
|
59
|
+
capabilities: CapabilityInfo[]
|
|
60
|
+
universal_capabilities?: CapabilityInfo[]
|
|
50
61
|
current_session: CurrentSession | null
|
|
51
62
|
sessions: Session[]
|
|
52
63
|
prompt?: string // The markdown body/prompt
|
|
@@ -40,6 +40,7 @@ module PromptObjects
|
|
|
40
40
|
:manifest, :env_path, :auto_commit, :session_store,
|
|
41
41
|
:current_provider, :current_model
|
|
42
42
|
attr_accessor :on_po_registered # Callback for when a PO is registered
|
|
43
|
+
attr_accessor :on_po_modified # Callback for when a PO is modified (capabilities changed, etc.)
|
|
43
44
|
|
|
44
45
|
# Initialize from an environment path (with manifest) or objects directory.
|
|
45
46
|
# @param env_path [String, nil] Path to environment directory (preferred)
|
|
@@ -207,6 +208,13 @@ module PromptObjects
|
|
|
207
208
|
po
|
|
208
209
|
end
|
|
209
210
|
|
|
211
|
+
# Notify that a PO has been modified (for live updates in web UI).
|
|
212
|
+
# Call this after modifying a PO's config/capabilities programmatically.
|
|
213
|
+
# @param po [PromptObject] The modified PO
|
|
214
|
+
def notify_po_modified(po)
|
|
215
|
+
@on_po_modified&.call(po)
|
|
216
|
+
end
|
|
217
|
+
|
|
210
218
|
# Load a prompt object by name from the objects directory.
|
|
211
219
|
# @param name [String] Name of the prompt object (without .md extension)
|
|
212
220
|
# @return [PromptObject]
|
|
@@ -267,13 +275,16 @@ module PromptObjects
|
|
|
267
275
|
@registry.register(Universal::Think.new)
|
|
268
276
|
@registry.register(Universal::CreateCapability.new)
|
|
269
277
|
@registry.register(Universal::AddCapability.new)
|
|
278
|
+
@registry.register(Universal::RemoveCapability.new)
|
|
270
279
|
@registry.register(Universal::ListCapabilities.new)
|
|
271
280
|
@registry.register(Universal::ListPrimitives.new)
|
|
272
281
|
@registry.register(Universal::AddPrimitive.new)
|
|
273
282
|
@registry.register(Universal::CreatePrimitive.new)
|
|
283
|
+
@registry.register(Universal::DeletePrimitive.new)
|
|
274
284
|
@registry.register(Universal::VerifyPrimitive.new)
|
|
275
285
|
@registry.register(Universal::ModifyPrimitive.new)
|
|
276
286
|
@registry.register(Universal::RequestPrimitive.new)
|
|
287
|
+
@registry.register(Universal::ModifyPrompt.new)
|
|
277
288
|
end
|
|
278
289
|
end
|
|
279
290
|
|
|
@@ -5,6 +5,7 @@ module PromptObjects
|
|
|
5
5
|
# It interprets messages semantically using its markdown "soul" as the system prompt.
|
|
6
6
|
class PromptObject < Capability
|
|
7
7
|
attr_reader :config, :body, :history, :session_id, :path
|
|
8
|
+
attr_accessor :on_history_updated # Callback for real-time updates during receive loop
|
|
8
9
|
|
|
9
10
|
# @param config [Hash] Parsed frontmatter (name, description, capabilities)
|
|
10
11
|
# @param body [String] Markdown body (the "soul" - becomes system prompt)
|
|
@@ -95,6 +96,9 @@ module PromptObjects
|
|
|
95
96
|
tool_msg = { role: :tool, results: results }
|
|
96
97
|
@history << tool_msg
|
|
97
98
|
persist_message(tool_msg)
|
|
99
|
+
|
|
100
|
+
# Notify callback for real-time UI updates (tool calls as they happen)
|
|
101
|
+
notify_history_updated
|
|
98
102
|
else
|
|
99
103
|
# No tool calls - we have our final response
|
|
100
104
|
assistant_msg = { role: :assistant, content: response.content }
|
|
@@ -341,6 +345,12 @@ module PromptObjects
|
|
|
341
345
|
messages.last&.dig(:id)
|
|
342
346
|
end
|
|
343
347
|
|
|
348
|
+
# Notify the history updated callback if registered.
|
|
349
|
+
# Used for real-time UI updates during the receive loop.
|
|
350
|
+
def notify_history_updated
|
|
351
|
+
@on_history_updated&.call(self, @session_id, @history)
|
|
352
|
+
end
|
|
353
|
+
|
|
344
354
|
# --- Session Persistence Helpers ---
|
|
345
355
|
|
|
346
356
|
# Load existing session or create a new one.
|