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
@@ -1,56 +0,0 @@
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
- {(() => {
45
- const text = msg.summary || (typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content))
46
- return text.length > 200 ? text.slice(0, 200) + '...' : text
47
- })()}
48
- </div>
49
- </div>
50
- ))
51
- )}
52
- <div ref={messagesEndRef} />
53
- </div>
54
- </div>
55
- )
56
- }
@@ -1,56 +0,0 @@
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
- }
@@ -1,124 +0,0 @@
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
- updatePrompt: (target: string, prompt: string) => void
13
- }
14
-
15
- export function PODetail({
16
- sendMessage,
17
- createSession,
18
- switchSession,
19
- createThread,
20
- updatePrompt,
21
- }: PODetailProps) {
22
- const { activeTab, setActiveTab, selectPO } = useStore()
23
- const po = useSelectedPO()
24
- const notifications = usePONotifications(po?.name || '')
25
-
26
- if (!po) {
27
- return (
28
- <div className="h-full flex items-center justify-center text-gray-500">
29
- Select a Prompt Object
30
- </div>
31
- )
32
- }
33
-
34
- const tabs = [
35
- { id: 'chat' as const, label: 'Chat' },
36
- { id: 'sessions' as const, label: `Threads (${po.sessions?.length || 0})` },
37
- { id: 'capabilities' as const, label: `Capabilities (${po.capabilities?.length || 0})` },
38
- { id: 'prompt' as const, label: 'Prompt' },
39
- ]
40
-
41
- return (
42
- <div className="h-full flex flex-col">
43
- {/* PO Header */}
44
- <div className="border-b border-po-border bg-po-surface px-4 py-3">
45
- <div className="flex items-center justify-between mb-2">
46
- <div className="flex items-center gap-3">
47
- <button
48
- onClick={() => selectPO(null)}
49
- className="text-gray-400 hover:text-white transition-colors"
50
- >
51
- ← Back
52
- </button>
53
- <h2 className="text-lg font-semibold text-white">{po.name}</h2>
54
- {notifications.length > 0 && (
55
- <span className="bg-po-warning text-black text-xs font-bold px-2 py-0.5 rounded-full">
56
- {notifications.length} pending
57
- </span>
58
- )}
59
- </div>
60
- <div className="flex items-center gap-2 text-sm">
61
- <StatusIndicator status={po.status} />
62
- </div>
63
- </div>
64
- <p className="text-sm text-gray-400">{po.description}</p>
65
- </div>
66
-
67
- {/* Tabs */}
68
- <div className="border-b border-po-border bg-po-surface px-4">
69
- <div className="flex gap-1">
70
- {tabs.map((tab) => (
71
- <button
72
- key={tab.id}
73
- onClick={() => setActiveTab(tab.id)}
74
- className={`px-4 py-2 text-sm font-medium transition-colors border-b-2 -mb-px ${
75
- activeTab === tab.id
76
- ? 'text-po-accent border-po-accent'
77
- : 'text-gray-400 border-transparent hover:text-white'
78
- }`}
79
- >
80
- {tab.label}
81
- </button>
82
- ))}
83
- </div>
84
- </div>
85
-
86
- {/* Tab content */}
87
- <div className="flex-1 overflow-hidden">
88
- {activeTab === 'chat' && (
89
- <ChatPanel po={po} sendMessage={sendMessage} />
90
- )}
91
- {activeTab === 'sessions' && (
92
- <SessionsPanel
93
- po={po}
94
- createSession={createSession}
95
- switchSession={switchSession}
96
- createThread={createThread}
97
- />
98
- )}
99
- {activeTab === 'capabilities' && <CapabilitiesPanel po={po} />}
100
- {activeTab === 'prompt' && (
101
- <PromptPanel
102
- po={po}
103
- onSave={(prompt) => updatePrompt(po.name, prompt)}
104
- />
105
- )}
106
- </div>
107
- </div>
108
- )
109
- }
110
-
111
- function StatusIndicator({ status }: { status: string }) {
112
- const config = {
113
- idle: { color: 'bg-gray-500', label: 'Idle' },
114
- thinking: { color: 'bg-po-accent animate-pulse', label: 'Thinking...' },
115
- calling_tool: { color: 'bg-po-warning animate-pulse', label: 'Calling tool...' },
116
- }[status] || { color: 'bg-gray-500', label: status }
117
-
118
- return (
119
- <div className="flex items-center gap-2">
120
- <div className={`w-2 h-2 rounded-full ${config.color}`} />
121
- <span className="text-gray-400">{config.label}</span>
122
- </div>
123
- )
124
- }
@@ -1,156 +0,0 @@
1
- import { useState, useEffect, useCallback, useRef } from 'react'
2
- import type { PromptObject } from '../types'
3
- import { MarkdownMessage } from './MarkdownMessage'
4
-
5
- interface PromptPanelProps {
6
- po: PromptObject
7
- onSave?: (prompt: string) => void
8
- }
9
-
10
- export function PromptPanel({ po, onSave }: PromptPanelProps) {
11
- const prompt = po.prompt || ''
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
- }, [])
76
-
77
- return (
78
- <div className="h-full overflow-auto p-4">
79
- {/* Config/Frontmatter */}
80
- <div className="mb-6">
81
- <h3 className="text-lg font-medium text-white mb-3">Configuration</h3>
82
- <div className="bg-po-bg rounded-lg border border-po-border p-4">
83
- <pre className="text-sm text-gray-300 font-mono whitespace-pre-wrap">
84
- {JSON.stringify(config, null, 2)}
85
- </pre>
86
- </div>
87
- </div>
88
-
89
- {/* Prompt/Body */}
90
- <div>
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
- />
127
- ) : (
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>
137
- )}
138
- </div>
139
- </div>
140
-
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
- )}
154
- </div>
155
- )
156
- }
@@ -1,174 +0,0 @@
1
- import { useMemo } from 'react'
2
- import type { PromptObject, Session, ThreadType } from '../types'
3
-
4
- interface SessionsPanelProps {
5
- po: PromptObject
6
- createSession: (target: string, name?: string) => void
7
- switchSession: (target: string, sessionId: string) => void
8
- createThread?: (target: string, name?: string) => void
9
- }
10
-
11
- // Build a tree structure from flat sessions list
12
- interface ThreadNode {
13
- session: Session
14
- children: ThreadNode[]
15
- depth: number
16
- }
17
-
18
- function buildThreadTree(sessions: Session[]): ThreadNode[] {
19
- const sessionMap = new Map<string, Session>()
20
- const childrenMap = new Map<string, Session[]>()
21
-
22
- // Index all sessions
23
- sessions.forEach((s) => {
24
- sessionMap.set(s.id, s)
25
- if (s.parent_session_id) {
26
- const children = childrenMap.get(s.parent_session_id) || []
27
- children.push(s)
28
- childrenMap.set(s.parent_session_id, children)
29
- }
30
- })
31
-
32
- // Build tree recursively
33
- function buildNode(session: Session, depth: number): ThreadNode {
34
- const children = childrenMap.get(session.id) || []
35
- return {
36
- session,
37
- children: children
38
- .sort((a, b) => (a.updated_at || '').localeCompare(b.updated_at || ''))
39
- .map((child) => buildNode(child, depth + 1)),
40
- depth,
41
- }
42
- }
43
-
44
- // Get root sessions (no parent)
45
- const roots = sessions
46
- .filter((s) => !s.parent_session_id)
47
- .sort((a, b) => (b.updated_at || '').localeCompare(a.updated_at || ''))
48
-
49
- return roots.map((root) => buildNode(root, 0))
50
- }
51
-
52
- // Flatten tree for rendering
53
- function flattenTree(nodes: ThreadNode[]): ThreadNode[] {
54
- const result: ThreadNode[] = []
55
- function traverse(node: ThreadNode) {
56
- result.push(node)
57
- node.children.forEach(traverse)
58
- }
59
- nodes.forEach(traverse)
60
- return result
61
- }
62
-
63
- // Thread type icons/badges
64
- function ThreadTypeBadge({ type }: { type: ThreadType }) {
65
- switch (type) {
66
- case 'delegation':
67
- return (
68
- <span className="text-xs bg-purple-600/30 text-purple-300 px-1.5 py-0.5 rounded">
69
- ↳ delegation
70
- </span>
71
- )
72
- case 'fork':
73
- return (
74
- <span className="text-xs bg-blue-600/30 text-blue-300 px-1.5 py-0.5 rounded">
75
- ⑂ fork
76
- </span>
77
- )
78
- case 'continuation':
79
- return (
80
- <span className="text-xs bg-gray-600/30 text-gray-300 px-1.5 py-0.5 rounded">
81
- → continued
82
- </span>
83
- )
84
- default:
85
- return null
86
- }
87
- }
88
-
89
- export function SessionsPanel({
90
- po,
91
- createSession,
92
- switchSession,
93
- createThread,
94
- }: SessionsPanelProps) {
95
- const sessions = po.sessions || []
96
- const currentSessionId = po.current_session?.id
97
-
98
- // Build and flatten thread tree
99
- const flatNodes = useMemo(() => {
100
- const tree = buildThreadTree(sessions)
101
- return flattenTree(tree)
102
- }, [sessions])
103
-
104
- const handleNewThread = () => {
105
- if (createThread) {
106
- createThread(po.name)
107
- } else {
108
- // Fallback to createSession
109
- const name = prompt('Thread name (optional):')
110
- createSession(po.name, name || undefined)
111
- }
112
- }
113
-
114
- return (
115
- <div className="h-full overflow-auto p-4">
116
- <div className="flex items-center justify-between mb-4">
117
- <h3 className="text-lg font-medium text-white">Threads</h3>
118
- <button
119
- onClick={handleNewThread}
120
- className="px-3 py-1.5 bg-po-accent text-white text-sm rounded hover:bg-po-accent/80 transition-colors"
121
- >
122
- + New Thread
123
- </button>
124
- </div>
125
-
126
- {flatNodes.length === 0 ? (
127
- <div className="text-gray-500 text-center py-8">
128
- No threads yet. Start a conversation to create one.
129
- </div>
130
- ) : (
131
- <div className="space-y-1">
132
- {flatNodes.map(({ session, depth }) => (
133
- <button
134
- key={session.id}
135
- onClick={() => switchSession(po.name, session.id)}
136
- className={`w-full text-left p-3 rounded-lg border transition-colors ${
137
- session.id === currentSessionId
138
- ? 'bg-po-accent/20 border-po-accent'
139
- : 'bg-po-surface border-po-border hover:border-po-accent/50'
140
- }`}
141
- style={{ marginLeft: `${depth * 16}px`, width: `calc(100% - ${depth * 16}px)` }}
142
- >
143
- <div className="flex items-center gap-2 mb-1">
144
- {depth > 0 && (
145
- <span className="text-gray-500 text-xs">↳</span>
146
- )}
147
- <span className="font-medium text-white truncate flex-1">
148
- {session.name || `Thread ${session.id.slice(0, 8)}`}
149
- </span>
150
- <ThreadTypeBadge type={session.thread_type} />
151
- {session.id === currentSessionId && (
152
- <span className="text-xs bg-po-accent text-white px-2 py-0.5 rounded flex-shrink-0">
153
- Active
154
- </span>
155
- )}
156
- </div>
157
- <div className="text-sm text-gray-400 flex items-center gap-2">
158
- <span>{session.message_count} messages</span>
159
- {session.parent_po && (
160
- <span className="text-purple-400">from {session.parent_po}</span>
161
- )}
162
- {session.updated_at && (
163
- <span className="ml-auto">
164
- {new Date(session.updated_at).toLocaleDateString()}
165
- </span>
166
- )}
167
- </div>
168
- </button>
169
- ))}
170
- </div>
171
- )}
172
- </div>
173
- )
174
- }