prompt_objects 0.3.1 → 0.5.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/CLAUDE.md +112 -44
  4. data/Gemfile.lock +31 -29
  5. data/README.md +5 -0
  6. data/frontend/index.html +5 -1
  7. data/frontend/package-lock.json +123 -0
  8. data/frontend/package.json +4 -0
  9. data/frontend/src/App.tsx +70 -71
  10. data/frontend/src/canvas/CanvasView.tsx +113 -0
  11. data/frontend/src/canvas/ForceLayout.ts +115 -0
  12. data/frontend/src/canvas/SceneManager.ts +587 -0
  13. data/frontend/src/canvas/canvasStore.ts +47 -0
  14. data/frontend/src/canvas/constants.ts +95 -0
  15. data/frontend/src/canvas/controls/CameraControls.ts +98 -0
  16. data/frontend/src/canvas/edges/MessageArc.ts +149 -0
  17. data/frontend/src/canvas/inspector/InspectorPanel.tsx +31 -0
  18. data/frontend/src/canvas/inspector/POInspector.tsx +262 -0
  19. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +67 -0
  20. data/frontend/src/canvas/nodes/PONode.ts +249 -0
  21. data/frontend/src/canvas/nodes/ToolCallNode.ts +116 -0
  22. data/frontend/src/canvas/types.ts +24 -0
  23. data/frontend/src/components/ContextMenu.tsx +5 -4
  24. data/frontend/src/components/Inspector.tsx +232 -0
  25. data/frontend/src/components/MarkdownMessage.tsx +22 -20
  26. data/frontend/src/components/MethodList.tsx +90 -0
  27. data/frontend/src/components/ModelSelector.tsx +13 -14
  28. data/frontend/src/components/NotificationPanel.tsx +29 -33
  29. data/frontend/src/components/ObjectList.tsx +78 -0
  30. data/frontend/src/components/PaneSlot.tsx +30 -0
  31. data/frontend/src/components/SourcePane.tsx +202 -0
  32. data/frontend/src/components/SystemBar.tsx +74 -0
  33. data/frontend/src/components/Transcript.tsx +76 -0
  34. data/frontend/src/components/UsagePanel.tsx +27 -27
  35. data/frontend/src/components/Workspace.tsx +260 -0
  36. data/frontend/src/components/index.ts +10 -9
  37. data/frontend/src/hooks/useResize.ts +55 -0
  38. data/frontend/src/hooks/useWebSocket.ts +274 -189
  39. data/frontend/src/index.css +69 -3
  40. data/frontend/src/store/index.ts +23 -0
  41. data/frontend/src/types/index.ts +5 -0
  42. data/frontend/tailwind.config.js +28 -9
  43. data/lib/prompt_objects/capability.rb +23 -1
  44. data/lib/prompt_objects/connectors/mcp.rb +5 -22
  45. data/lib/prompt_objects/environment.rb +8 -0
  46. data/lib/prompt_objects/llm/openai_adapter.rb +22 -0
  47. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +1 -2
  48. data/lib/prompt_objects/mcp/tools/inspect_po.rb +1 -31
  49. data/lib/prompt_objects/mcp/tools/list_prompt_objects.rb +2 -8
  50. data/lib/prompt_objects/primitives/list_files.rb +1 -2
  51. data/lib/prompt_objects/prompt_object.rb +150 -6
  52. data/lib/prompt_objects/server/api/routes.rb +3 -48
  53. data/lib/prompt_objects/server/app.rb +9 -0
  54. data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +4345 -0
  55. data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +1 -0
  56. data/lib/prompt_objects/server/public/index.html +7 -3
  57. data/lib/prompt_objects/server/websocket_handler.rb +23 -100
  58. data/lib/prompt_objects/server.rb +6 -62
  59. data/prompt_objects.gemspec +1 -1
  60. data/templates/arc-agi-1/primitives/find_objects.rb +1 -1
  61. data/templates/arc-agi-1/primitives/grid_diff.rb +2 -2
  62. data/templates/arc-agi-1/primitives/grid_info.rb +1 -1
  63. data/templates/arc-agi-1/primitives/grid_transform.rb +1 -1
  64. data/templates/arc-agi-1/primitives/render_grid.rb +1 -0
  65. data/templates/arc-agi-1/primitives/test_solution.rb +3 -0
  66. metadata +26 -14
  67. data/frontend/src/components/CapabilitiesPanel.tsx +0 -141
  68. data/frontend/src/components/ChatPanel.tsx +0 -288
  69. data/frontend/src/components/Dashboard.tsx +0 -83
  70. data/frontend/src/components/Header.tsx +0 -141
  71. data/frontend/src/components/MessageBus.tsx +0 -56
  72. data/frontend/src/components/POCard.tsx +0 -56
  73. data/frontend/src/components/PODetail.tsx +0 -124
  74. data/frontend/src/components/PromptPanel.tsx +0 -156
  75. data/frontend/src/components/SessionsPanel.tsx +0 -174
  76. data/frontend/src/components/ThreadsSidebar.tsx +0 -163
  77. data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +0 -1
  78. data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +0 -77
@@ -30,7 +30,7 @@ export function MarkdownMessage({ content, className = '' }: MarkdownMessageProp
30
30
 
31
31
  return (
32
32
  <code
33
- className="bg-po-bg px-1.5 py-0.5 rounded text-po-accent font-mono text-sm"
33
+ className="bg-po-surface-2 px-1 py-0.5 rounded text-po-accent font-mono text-[0.9em]"
34
34
  {...props}
35
35
  >
36
36
  {children}
@@ -52,29 +52,29 @@ export function MarkdownMessage({ content, className = '' }: MarkdownMessageProp
52
52
  },
53
53
  // Styled paragraphs
54
54
  p({ children }) {
55
- return <p className="mb-3 last:mb-0">{children}</p>
55
+ return <p className="mb-2 last:mb-0">{children}</p>
56
56
  },
57
57
  // Styled lists
58
58
  ul({ children }) {
59
- return <ul className="list-disc list-inside mb-3 space-y-1">{children}</ul>
59
+ return <ul className="list-disc list-inside mb-2 space-y-0.5">{children}</ul>
60
60
  },
61
61
  ol({ children }) {
62
- return <ol className="list-decimal list-inside mb-3 space-y-1">{children}</ol>
62
+ return <ol className="list-decimal list-inside mb-2 space-y-0.5">{children}</ol>
63
63
  },
64
64
  // Styled headings
65
65
  h1({ children }) {
66
- return <h1 className="text-xl font-bold mb-2 mt-4 first:mt-0">{children}</h1>
66
+ return <h1 className="text-base font-bold mb-1.5 mt-3 first:mt-0 text-po-text-primary">{children}</h1>
67
67
  },
68
68
  h2({ children }) {
69
- return <h2 className="text-lg font-bold mb-2 mt-3 first:mt-0">{children}</h2>
69
+ return <h2 className="text-sm font-bold mb-1.5 mt-2 first:mt-0 text-po-text-primary">{children}</h2>
70
70
  },
71
71
  h3({ children }) {
72
- return <h3 className="text-base font-bold mb-2 mt-2 first:mt-0">{children}</h3>
72
+ return <h3 className="text-xs font-bold mb-1 mt-1.5 first:mt-0 text-po-text-primary">{children}</h3>
73
73
  },
74
74
  // Styled blockquotes
75
75
  blockquote({ children }) {
76
76
  return (
77
- <blockquote className="border-l-4 border-po-accent pl-4 my-3 text-gray-400 italic">
77
+ <blockquote className="border-l-2 border-po-accent pl-3 my-2 text-po-text-secondary italic">
78
78
  {children}
79
79
  </blockquote>
80
80
  )
@@ -82,26 +82,26 @@ export function MarkdownMessage({ content, className = '' }: MarkdownMessageProp
82
82
  // Styled tables
83
83
  table({ children }) {
84
84
  return (
85
- <div className="overflow-x-auto my-3">
86
- <table className="min-w-full border border-po-border">{children}</table>
85
+ <div className="overflow-x-auto my-2">
86
+ <table className="min-w-full border border-po-border text-xs">{children}</table>
87
87
  </div>
88
88
  )
89
89
  },
90
90
  th({ children }) {
91
91
  return (
92
- <th className="border border-po-border bg-po-bg px-3 py-2 text-left font-medium">
92
+ <th className="border border-po-border bg-po-surface-2 px-2 py-1 text-left font-medium text-po-text-primary">
93
93
  {children}
94
94
  </th>
95
95
  )
96
96
  },
97
97
  td({ children }) {
98
98
  return (
99
- <td className="border border-po-border px-3 py-2">{children}</td>
99
+ <td className="border border-po-border px-2 py-1 text-po-text-secondary">{children}</td>
100
100
  )
101
101
  },
102
102
  // Horizontal rule
103
103
  hr() {
104
- return <hr className="border-po-border my-4" />
104
+ return <hr className="border-po-border my-3" />
105
105
  },
106
106
  }}
107
107
  >
@@ -121,17 +121,17 @@ function CodeBlock({ language, code }: { language: string; code: string }) {
121
121
  }
122
122
 
123
123
  return (
124
- <div className="relative group my-3">
124
+ <div className="relative group my-2">
125
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">
126
+ <div className="flex items-center justify-between bg-po-surface-2 px-2.5 py-1 rounded-t border border-b-0 border-po-border">
127
+ <span className="text-2xs text-po-text-ghost font-mono">
128
128
  {language || 'text'}
129
129
  </span>
130
130
  <button
131
131
  onClick={handleCopy}
132
- className="text-xs text-gray-400 hover:text-white transition-colors"
132
+ className="text-2xs text-po-text-ghost hover:text-po-text-primary transition-colors duration-150"
133
133
  >
134
- {copied ? 'Copied!' : 'Copy'}
134
+ {copied ? 'Copied' : 'Copy'}
135
135
  </button>
136
136
  </div>
137
137
  {/* Code with syntax highlighting */}
@@ -141,9 +141,11 @@ function CodeBlock({ language, code }: { language: string; code: string }) {
141
141
  PreTag="div"
142
142
  customStyle={{
143
143
  margin: 0,
144
- borderRadius: '0 0 0.375rem 0.375rem',
145
- border: '1px solid rgb(55, 65, 81)',
144
+ borderRadius: '0 0 0.25rem 0.25rem',
145
+ border: '1px solid #3d3a37',
146
146
  borderTop: 'none',
147
+ fontSize: '11px',
148
+ background: '#222120',
147
149
  }}
148
150
  >
149
151
  {code}
@@ -0,0 +1,90 @@
1
+ import type { PromptObject, CapabilityInfo } from '../types'
2
+
3
+ interface MethodListProps {
4
+ po: PromptObject
5
+ selectedCapability: CapabilityInfo | null
6
+ onSelectCapability: (cap: CapabilityInfo | null) => void
7
+ }
8
+
9
+ export function MethodList({ po, selectedCapability, onSelectCapability }: MethodListProps) {
10
+ const capabilities = po.capabilities || []
11
+ const universalCapabilities = po.universal_capabilities || []
12
+
13
+ const handleClick = (cap: CapabilityInfo) => {
14
+ if (selectedCapability?.name === cap.name) {
15
+ onSelectCapability(null) // Toggle off
16
+ } else {
17
+ onSelectCapability(cap)
18
+ }
19
+ }
20
+
21
+ return (
22
+ <div className="h-full border-r border-po-border overflow-auto bg-po-surface">
23
+ {/* Source (prompt view) */}
24
+ <button
25
+ onClick={() => onSelectCapability(null)}
26
+ className={`w-full text-left px-2.5 py-1 text-xs font-mono border-b border-po-border transition-colors duration-150 ${
27
+ selectedCapability === null
28
+ ? 'bg-po-accent-wash text-po-accent'
29
+ : 'text-po-text-secondary hover:bg-po-surface-2'
30
+ }`}
31
+ >
32
+ Source
33
+ </button>
34
+
35
+ {/* Declared capabilities */}
36
+ {capabilities.length > 0 && (
37
+ <div>
38
+ <div className="px-2.5 py-1.5 border-b border-po-border">
39
+ <span className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider">
40
+ Methods ({capabilities.length})
41
+ </span>
42
+ </div>
43
+ {capabilities.map((cap) => (
44
+ <button
45
+ key={cap.name}
46
+ onClick={() => handleClick(cap)}
47
+ className={`w-full text-left px-2.5 py-1 text-xs font-mono transition-colors duration-150 ${
48
+ selectedCapability?.name === cap.name
49
+ ? 'bg-po-accent-wash text-po-accent'
50
+ : 'text-po-accent hover:bg-po-surface-2'
51
+ }`}
52
+ >
53
+ {cap.name}
54
+ </button>
55
+ ))}
56
+ </div>
57
+ )}
58
+
59
+ {/* Universal capabilities */}
60
+ {universalCapabilities.length > 0 && (
61
+ <div>
62
+ <div className="px-2.5 py-1.5 border-b border-po-border border-t border-po-border">
63
+ <span className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider">
64
+ Universal ({universalCapabilities.length})
65
+ </span>
66
+ </div>
67
+ {universalCapabilities.map((cap) => (
68
+ <button
69
+ key={cap.name}
70
+ onClick={() => handleClick(cap)}
71
+ className={`w-full text-left px-2.5 py-1 text-xs font-mono transition-colors duration-150 ${
72
+ selectedCapability?.name === cap.name
73
+ ? 'bg-po-accent-wash text-po-text-secondary'
74
+ : 'text-po-text-secondary hover:bg-po-surface-2'
75
+ }`}
76
+ >
77
+ {cap.name}
78
+ </button>
79
+ ))}
80
+ </div>
81
+ )}
82
+
83
+ {capabilities.length === 0 && universalCapabilities.length === 0 && (
84
+ <div className="px-2.5 py-4 text-2xs text-po-text-ghost text-center">
85
+ No capabilities
86
+ </div>
87
+ )}
88
+ </div>
89
+ )
90
+ }
@@ -28,7 +28,6 @@ export function ModelSelector({ switchLLM }: Props) {
28
28
  setIsOpen(false)
29
29
  }
30
30
 
31
- // Provider display names
32
31
  const providerNames: Record<string, string> = {
33
32
  openai: 'OpenAI',
34
33
  anthropic: 'Anthropic',
@@ -41,14 +40,14 @@ export function ModelSelector({ switchLLM }: Props) {
41
40
  <div className="relative" ref={dropdownRef}>
42
41
  <button
43
42
  onClick={() => setIsOpen(!isOpen)}
44
- 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
+ className="flex items-center gap-1.5 px-2 py-0.5 text-xs bg-po-surface-2 border border-po-border rounded hover:border-po-border-focus transition-colors duration-150"
45
44
  >
46
- <span className="text-gray-400">
45
+ <span className="text-po-text-tertiary">
47
46
  {providerNames[llmConfig.current_provider] || llmConfig.current_provider}
48
47
  </span>
49
- <span className="text-white font-medium">{llmConfig.current_model}</span>
48
+ <span className="font-mono text-po-text-primary">{llmConfig.current_model}</span>
50
49
  <svg
51
- className={`w-4 h-4 text-gray-400 transition-transform ${isOpen ? 'rotate-180' : ''}`}
50
+ className={`w-3 h-3 text-po-text-ghost transition-transform ${isOpen ? 'rotate-180' : ''}`}
52
51
  fill="none"
53
52
  stroke="currentColor"
54
53
  viewBox="0 0 24 24"
@@ -58,13 +57,13 @@ export function ModelSelector({ switchLLM }: Props) {
58
57
  </button>
59
58
 
60
59
  {isOpen && (
61
- <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
+ <div className="absolute right-0 top-full mt-1 w-60 bg-po-surface-2 border border-po-border rounded shadow-xl z-50 overflow-hidden">
62
61
  {llmConfig.providers.map((provider) => (
63
62
  <div key={provider.name}>
64
- <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
+ <div className="px-2.5 py-1.5 bg-po-surface text-2xs font-medium text-po-text-ghost uppercase tracking-wider flex items-center justify-between">
65
64
  <span>{providerNames[provider.name] || provider.name}</span>
66
65
  {!provider.available && (
67
- <span className="text-red-400 text-[10px] normal-case">
66
+ <span className="text-po-error text-2xs normal-case">
68
67
  {provider.name === 'ollama' ? 'Not Running' : 'No API Key'}
69
68
  </span>
70
69
  )}
@@ -81,22 +80,22 @@ export function ModelSelector({ switchLLM }: Props) {
81
80
  key={`${provider.name}-${model}`}
82
81
  onClick={() => isAvailable && handleSelectModel(provider.name, model)}
83
82
  disabled={!isAvailable}
84
- className={`w-full px-3 py-2 text-left text-sm flex items-center justify-between transition-colors ${
83
+ className={`w-full px-2.5 py-1.5 text-left text-xs font-mono flex items-center justify-between transition-colors duration-150 ${
85
84
  isSelected
86
- ? 'bg-po-accent/20 text-po-accent'
85
+ ? 'bg-po-accent-wash text-po-accent'
87
86
  : isAvailable
88
- ? 'text-gray-300 hover:bg-po-border'
89
- : 'text-gray-600 cursor-not-allowed'
87
+ ? 'text-po-text-secondary hover:bg-po-surface-3'
88
+ : 'text-po-text-ghost cursor-not-allowed'
90
89
  }`}
91
90
  >
92
91
  <span className="flex items-center gap-2">
93
92
  {model}
94
93
  {isDefault && (
95
- <span className="text-[10px] text-gray-500">(default)</span>
94
+ <span className="text-2xs text-po-text-ghost">(default)</span>
96
95
  )}
97
96
  </span>
98
97
  {isSelected && (
99
- <svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
98
+ <svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
100
99
  <path
101
100
  fillRule="evenodd"
102
101
  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"
@@ -5,28 +5,28 @@ interface NotificationPanelProps {
5
5
  respondToNotification: (id: string, response: string) => void
6
6
  }
7
7
 
8
- export function NotificationPanel({
9
- respondToNotification,
10
- }: NotificationPanelProps) {
8
+ export function NotificationPanel({ respondToNotification }: NotificationPanelProps) {
11
9
  const { notifications, selectPO } = useStore()
12
10
 
13
11
  if (notifications.length === 0) return null
14
12
 
15
13
  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>
14
+ <div className="fixed bottom-4 right-4 w-96 max-h-[60vh] overflow-auto bg-po-surface-2 border border-po-warning/30 rounded-lg shadow-2xl z-50">
15
+ {/* Header */}
16
+ <div className="sticky top-0 bg-po-surface-2 border-b border-po-border px-3 py-2 flex items-center gap-2">
17
+ <div className="w-2 h-2 rounded-full bg-po-warning animate-pulse" />
18
+ <span className="text-xs font-medium text-po-text-primary flex-1">
19
+ Pending Requests ({notifications.length})
20
+ </span>
21
21
  </div>
22
+
23
+ {/* Notifications */}
22
24
  <div className="p-2 space-y-2">
23
25
  {notifications.map((notification) => (
24
26
  <NotificationCard
25
27
  key={notification.id}
26
28
  notification={notification}
27
- onRespond={(response) =>
28
- respondToNotification(notification.id, response)
29
- }
29
+ onRespond={(response) => respondToNotification(notification.id, response)}
30
30
  onViewPO={() => selectPO(notification.po_name)}
31
31
  />
32
32
  ))}
@@ -47,18 +47,10 @@ interface NotificationCardProps {
47
47
  onViewPO: () => void
48
48
  }
49
49
 
50
- function NotificationCard({
51
- notification,
52
- onRespond,
53
- onViewPO,
54
- }: NotificationCardProps) {
50
+ function NotificationCard({ notification, onRespond, onViewPO }: NotificationCardProps) {
55
51
  const [customInput, setCustomInput] = useState('')
56
52
  const [showCustom, setShowCustom] = useState(false)
57
53
 
58
- const handleOptionClick = (option: string) => {
59
- onRespond(option)
60
- }
61
-
62
54
  const handleCustomSubmit = () => {
63
55
  if (customInput.trim()) {
64
56
  onRespond(customInput.trim())
@@ -68,28 +60,31 @@ function NotificationCard({
68
60
  }
69
61
 
70
62
  return (
71
- <div className="bg-po-bg border border-po-border rounded-lg p-3">
63
+ <div className="bg-po-surface border border-po-border rounded-lg p-3">
64
+ {/* Header: type badge + PO name */}
72
65
  <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">
66
+ <span className="text-2xs font-mono bg-po-warning text-po-bg px-1.5 py-0.5 rounded font-bold">
74
67
  {notification.type}
75
68
  </span>
76
69
  <button
77
70
  onClick={onViewPO}
78
- className="text-xs text-po-accent hover:underline"
71
+ className="text-xs font-mono text-po-accent hover:underline"
79
72
  >
80
73
  {notification.po_name}
81
74
  </button>
82
75
  </div>
83
76
 
84
- <p className="text-sm text-gray-200 mb-3">{notification.message}</p>
77
+ {/* Message */}
78
+ <p className="text-xs text-po-text-primary mb-3">{notification.message}</p>
85
79
 
80
+ {/* Quick response options */}
86
81
  {notification.options.length > 0 && (
87
- <div className="flex flex-wrap gap-2 mb-2">
82
+ <div className="flex flex-wrap gap-1.5 mb-2">
88
83
  {notification.options.map((option, index) => (
89
84
  <button
90
85
  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"
86
+ onClick={() => onRespond(option)}
87
+ className="px-2.5 py-1 text-xs bg-po-surface-2 border border-po-border rounded hover:border-po-accent hover:text-po-accent transition-colors duration-150 text-po-text-secondary"
93
88
  >
94
89
  {option}
95
90
  </button>
@@ -97,34 +92,35 @@ function NotificationCard({
97
92
  </div>
98
93
  )}
99
94
 
95
+ {/* Custom response */}
100
96
  {showCustom ? (
101
- <div className="flex gap-2">
97
+ <div className="flex gap-1.5">
102
98
  <input
103
99
  type="text"
104
100
  value={customInput}
105
101
  onChange={(e) => setCustomInput(e.target.value)}
106
102
  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"
103
+ className="flex-1 bg-po-surface-2 border border-po-border rounded px-2 py-1 text-xs text-po-text-primary placeholder-po-text-ghost focus:outline-none focus:border-po-accent"
108
104
  onKeyDown={(e) => e.key === 'Enter' && handleCustomSubmit()}
109
105
  autoFocus
110
106
  />
111
107
  <button
112
108
  onClick={handleCustomSubmit}
113
- className="px-2 py-1 text-sm bg-po-accent text-white rounded hover:bg-po-accent/80"
109
+ className="px-2 py-1 text-xs bg-po-accent text-po-bg rounded font-medium hover:bg-po-accent-muted transition-colors duration-150"
114
110
  >
115
111
  Send
116
112
  </button>
117
113
  <button
118
114
  onClick={() => setShowCustom(false)}
119
- className="px-2 py-1 text-sm text-gray-400 hover:text-white"
115
+ className="text-po-text-ghost hover:text-po-text-secondary transition-colors duration-150"
120
116
  >
121
- Cancel
117
+ {'\u2715'}
122
118
  </button>
123
119
  </div>
124
120
  ) : (
125
121
  <button
126
122
  onClick={() => setShowCustom(true)}
127
- className="text-xs text-gray-400 hover:text-white"
123
+ className="text-2xs text-po-text-ghost hover:text-po-text-secondary transition-colors duration-150"
128
124
  >
129
125
  + Custom response
130
126
  </button>
@@ -0,0 +1,78 @@
1
+ import { usePromptObjects, useStore, usePONotifications } from '../store'
2
+ import type { PromptObject } from '../types'
3
+
4
+ export function ObjectList() {
5
+ const promptObjects = usePromptObjects()
6
+
7
+ return (
8
+ <aside className="h-full bg-po-surface border-r border-po-border flex flex-col overflow-hidden">
9
+ {/* Header */}
10
+ <div className="px-3 py-2 border-b border-po-border">
11
+ <span className="text-2xs font-medium text-po-text-ghost uppercase tracking-wider">
12
+ Objects ({promptObjects.length})
13
+ </span>
14
+ </div>
15
+
16
+ {/* List */}
17
+ <div className="flex-1 overflow-auto py-1">
18
+ {promptObjects.length === 0 ? (
19
+ <div className="px-3 py-4 text-2xs text-po-text-ghost text-center">
20
+ Waiting for connection...
21
+ </div>
22
+ ) : (
23
+ promptObjects.map((po) => (
24
+ <ObjectItem key={po.name} po={po} />
25
+ ))
26
+ )}
27
+ </div>
28
+ </aside>
29
+ )
30
+ }
31
+
32
+ function ObjectItem({ po }: { po: PromptObject }) {
33
+ const { selectPO, selectedPO } = useStore()
34
+ const notifications = usePONotifications(po.name)
35
+ const isSelected = selectedPO === po.name
36
+
37
+ const isActive = po.status !== 'idle'
38
+
39
+ const statusDot = {
40
+ idle: 'bg-po-status-idle',
41
+ thinking: 'bg-po-status-active',
42
+ calling_tool: 'bg-po-status-calling',
43
+ }[po.status] || 'bg-po-status-idle'
44
+
45
+ const statusGlow = {
46
+ idle: '',
47
+ thinking: 'shadow-[0_0_5px_rgba(212,149,42,0.6)]',
48
+ calling_tool: 'shadow-[0_0_5px_rgba(59,154,110,0.6)]',
49
+ }[po.status] || ''
50
+
51
+ return (
52
+ <button
53
+ onClick={() => selectPO(po.name)}
54
+ className={`w-full text-left h-7 px-3 flex items-center gap-2 transition-colors duration-150 ${
55
+ isSelected
56
+ ? 'bg-po-accent-wash border-l-2 border-po-accent'
57
+ : 'border-l-2 border-transparent hover:bg-po-surface-2'
58
+ }`}
59
+ >
60
+ <div className={`w-2 h-2 rounded-full flex-shrink-0 ${statusDot} ${statusGlow} ${isActive ? 'animate-pulse' : ''}`} />
61
+ <span className={`flex-1 truncate font-mono text-xs ${
62
+ isSelected ? 'text-po-text-primary' : 'text-po-text-secondary'
63
+ }`}>
64
+ {po.name}
65
+ </span>
66
+ {po.delegated_by && (
67
+ <span className="text-2xs text-po-status-delegated truncate max-w-[60px]">
68
+ {po.delegated_by}
69
+ </span>
70
+ )}
71
+ {notifications.length > 0 && (
72
+ <span className="text-2xs font-mono bg-po-warning text-po-bg px-1 rounded font-bold flex-shrink-0">
73
+ {notifications.length}
74
+ </span>
75
+ )}
76
+ </button>
77
+ )
78
+ }
@@ -0,0 +1,30 @@
1
+ interface PaneSlotProps {
2
+ label: string
3
+ collapsed: boolean
4
+ onToggle: () => void
5
+ height: number
6
+ resizeHandle?: React.ReactNode
7
+ children: React.ReactNode
8
+ }
9
+
10
+ export function PaneSlot({ label, collapsed, onToggle, height, resizeHandle, children }: PaneSlotProps) {
11
+ return (
12
+ <>
13
+ <div
14
+ className="h-7 bg-po-surface-2 border-b border-po-border flex items-center px-3 cursor-pointer hover:bg-po-surface-3 transition-colors duration-150 flex-shrink-0 select-none"
15
+ onClick={onToggle}
16
+ >
17
+ <span className="text-2xs font-mono text-po-text-secondary flex-1">{label}</span>
18
+ <span className="text-xs text-po-text-ghost">{collapsed ? '▼' : '▲'}</span>
19
+ </div>
20
+ {!collapsed && (
21
+ <>
22
+ <div className="flex-shrink-0 overflow-hidden" style={{ height }}>
23
+ {children}
24
+ </div>
25
+ {resizeHandle}
26
+ </>
27
+ )}
28
+ </>
29
+ )
30
+ }