prompt_objects 0.2.0 → 0.3.1
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/CHANGELOG.md +80 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/exe/prompt_objects +548 -1
- data/frontend/src/App.tsx +11 -3
- data/frontend/src/components/ContextMenu.tsx +67 -0
- data/frontend/src/components/MessageBus.tsx +4 -3
- data/frontend/src/components/ModelSelector.tsx +5 -1
- data/frontend/src/components/ThreadsSidebar.tsx +46 -2
- data/frontend/src/components/UsagePanel.tsx +105 -0
- data/frontend/src/hooks/useWebSocket.ts +53 -0
- data/frontend/src/store/index.ts +10 -0
- data/frontend/src/types/index.ts +4 -1
- data/lib/prompt_objects/cli.rb +1 -0
- data/lib/prompt_objects/connectors/mcp.rb +1 -0
- data/lib/prompt_objects/environment.rb +24 -1
- data/lib/prompt_objects/llm/anthropic_adapter.rb +15 -1
- data/lib/prompt_objects/llm/factory.rb +93 -6
- data/lib/prompt_objects/llm/gemini_adapter.rb +13 -1
- data/lib/prompt_objects/llm/openai_adapter.rb +21 -4
- data/lib/prompt_objects/llm/pricing.rb +49 -0
- data/lib/prompt_objects/llm/response.rb +3 -2
- data/lib/prompt_objects/mcp/server.rb +1 -0
- data/lib/prompt_objects/message_bus.rb +27 -8
- data/lib/prompt_objects/prompt_object.rb +6 -4
- data/lib/prompt_objects/server/api/routes.rb +186 -29
- data/lib/prompt_objects/server/public/assets/index-Bkme6COu.css +1 -0
- data/lib/prompt_objects/server/public/assets/index-CQ7lVDF_.js +77 -0
- data/lib/prompt_objects/server/public/index.html +2 -2
- data/lib/prompt_objects/server/websocket_handler.rb +93 -9
- data/lib/prompt_objects/server.rb +54 -0
- data/lib/prompt_objects/session/store.rb +399 -4
- data/lib/prompt_objects.rb +1 -0
- data/prompt_objects.gemspec +1 -1
- data/templates/arc-agi-1/manifest.yml +22 -0
- data/templates/arc-agi-1/objects/data_manager.md +42 -0
- data/templates/arc-agi-1/objects/observer.md +100 -0
- data/templates/arc-agi-1/objects/solver.md +118 -0
- data/templates/arc-agi-1/objects/verifier.md +79 -0
- data/templates/arc-agi-1/primitives/check_arc_data.rb +53 -0
- data/templates/arc-agi-1/primitives/find_objects.rb +72 -0
- data/templates/arc-agi-1/primitives/grid_diff.rb +70 -0
- data/templates/arc-agi-1/primitives/grid_info.rb +42 -0
- data/templates/arc-agi-1/primitives/grid_transform.rb +50 -0
- data/templates/arc-agi-1/primitives/load_arc_task.rb +68 -0
- data/templates/arc-agi-1/primitives/render_grid.rb +78 -0
- data/templates/arc-agi-1/primitives/test_solution.rb +131 -0
- data/tools/thread-explorer.html +1043 -0
- metadata +21 -3
- data/lib/prompt_objects/server/public/assets/index-CeNJvqLG.js +0 -77
- data/lib/prompt_objects/server/public/assets/index-Vx4-uMOU.css +0 -1
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PromptObjects
|
|
4
|
+
module LLM
|
|
5
|
+
# Static pricing table for cost estimation.
|
|
6
|
+
# Prices are per 1 million tokens in USD.
|
|
7
|
+
# Updated periodically — not guaranteed to be exact.
|
|
8
|
+
class Pricing
|
|
9
|
+
RATES = {
|
|
10
|
+
# OpenAI
|
|
11
|
+
"gpt-5.2" => { input: 2.00, output: 8.00 },
|
|
12
|
+
"gpt-4.1" => { input: 2.00, output: 8.00 },
|
|
13
|
+
"gpt-4.1-mini" => { input: 0.40, output: 1.60 },
|
|
14
|
+
"gpt-4.5-preview" => { input: 75.00, output: 150.00 },
|
|
15
|
+
"o3-mini" => { input: 1.10, output: 4.40 },
|
|
16
|
+
"o1" => { input: 15.00, output: 60.00 },
|
|
17
|
+
# Anthropic
|
|
18
|
+
"claude-opus-4" => { input: 15.00, output: 75.00 },
|
|
19
|
+
"claude-sonnet-4-5" => { input: 3.00, output: 15.00 },
|
|
20
|
+
"claude-haiku-4-5" => { input: 1.00, output: 5.00 },
|
|
21
|
+
# Gemini
|
|
22
|
+
"gemini-3-flash-preview" => { input: 0.15, output: 0.60 },
|
|
23
|
+
"gemini-2.5-pro" => { input: 1.25, output: 10.00 },
|
|
24
|
+
"gemini-2.5-flash" => { input: 0.15, output: 0.60 },
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# Calculate cost in USD for a given usage.
|
|
28
|
+
# @param model [String] Model name
|
|
29
|
+
# @param input_tokens [Integer] Number of input tokens
|
|
30
|
+
# @param output_tokens [Integer] Number of output tokens
|
|
31
|
+
# @return [Float] Estimated cost in USD
|
|
32
|
+
def self.calculate(model:, input_tokens:, output_tokens:)
|
|
33
|
+
rates = RATES[model]
|
|
34
|
+
return 0.0 unless rates
|
|
35
|
+
|
|
36
|
+
input_cost = (input_tokens / 1_000_000.0) * rates[:input]
|
|
37
|
+
output_cost = (output_tokens / 1_000_000.0) * rates[:output]
|
|
38
|
+
input_cost + output_cost
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Check if we have pricing data for a model.
|
|
42
|
+
# @param model [String] Model name
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def self.known_model?(model)
|
|
45
|
+
RATES.key?(model)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -5,12 +5,13 @@ module PromptObjects
|
|
|
5
5
|
# Normalized response from an LLM API call.
|
|
6
6
|
# Wraps provider-specific responses into a common interface.
|
|
7
7
|
class Response
|
|
8
|
-
attr_reader :content, :tool_calls, :raw
|
|
8
|
+
attr_reader :content, :tool_calls, :raw, :usage
|
|
9
9
|
|
|
10
|
-
def initialize(content:, tool_calls: [], raw: nil)
|
|
10
|
+
def initialize(content:, tool_calls: [], raw: nil, usage: nil)
|
|
11
11
|
@content = content
|
|
12
12
|
@tool_calls = tool_calls
|
|
13
13
|
@raw = raw
|
|
14
|
+
@usage = usage # { input_tokens:, output_tokens:, model:, provider: }
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
# Check if the response includes tool calls.
|
|
@@ -4,28 +4,36 @@ module PromptObjects
|
|
|
4
4
|
# Message bus for routing and logging all inter-capability communication.
|
|
5
5
|
# This makes the semantic binding visible - you can see natural language
|
|
6
6
|
# being transformed into capability calls.
|
|
7
|
+
#
|
|
8
|
+
# Each entry stores the full message content and a truncated summary.
|
|
9
|
+
# Use :summary for compact log displays, :message for full inspection.
|
|
7
10
|
class MessageBus
|
|
8
11
|
attr_reader :log
|
|
9
12
|
|
|
10
|
-
|
|
13
|
+
# @param session_store [Session::Store, nil] Optional store for persistent event logging
|
|
14
|
+
def initialize(session_store: nil)
|
|
11
15
|
@log = []
|
|
12
16
|
@subscribers = []
|
|
17
|
+
@store = session_store
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
# Log a message between capabilities.
|
|
16
21
|
# @param from [String] Source capability name
|
|
17
22
|
# @param to [String] Destination capability name
|
|
18
|
-
# @param message [String, Hash] The message content
|
|
23
|
+
# @param message [String, Hash] The message content (stored in full)
|
|
24
|
+
# @param session_id [String, nil] Optional session ID for event persistence
|
|
19
25
|
# @return [Hash] The log entry
|
|
20
|
-
def publish(from:, to:, message:)
|
|
26
|
+
def publish(from:, to:, message:, session_id: nil)
|
|
21
27
|
entry = {
|
|
22
28
|
timestamp: Time.now,
|
|
23
29
|
from: from,
|
|
24
30
|
to: to,
|
|
25
|
-
message:
|
|
31
|
+
message: message,
|
|
32
|
+
summary: summarize(message)
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
@log << entry
|
|
36
|
+
persist_event(entry, session_id: session_id)
|
|
29
37
|
notify_subscribers(entry)
|
|
30
38
|
entry
|
|
31
39
|
end
|
|
@@ -54,7 +62,7 @@ module PromptObjects
|
|
|
54
62
|
@log.clear
|
|
55
63
|
end
|
|
56
64
|
|
|
57
|
-
# Format log entries for display.
|
|
65
|
+
# Format log entries for compact display.
|
|
58
66
|
# @param count [Integer] Number of entries to format
|
|
59
67
|
# @return [String]
|
|
60
68
|
def format_log(count = 20)
|
|
@@ -62,7 +70,7 @@ module PromptObjects
|
|
|
62
70
|
time = entry[:timestamp].strftime("%H:%M:%S")
|
|
63
71
|
from = entry[:from]
|
|
64
72
|
to = entry[:to]
|
|
65
|
-
msg = entry[:
|
|
73
|
+
msg = entry[:summary]
|
|
66
74
|
|
|
67
75
|
"#{time} #{from} → #{to}: #{msg}"
|
|
68
76
|
end.join("\n")
|
|
@@ -74,7 +82,18 @@ module PromptObjects
|
|
|
74
82
|
@subscribers.each { |s| s.call(entry) }
|
|
75
83
|
end
|
|
76
84
|
|
|
77
|
-
|
|
85
|
+
# Persist event to the session store (if available).
|
|
86
|
+
def persist_event(entry, session_id: nil)
|
|
87
|
+
return unless @store
|
|
88
|
+
|
|
89
|
+
@store.add_event(entry, session_id: session_id)
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
# Don't let persistence failures break the bus
|
|
92
|
+
warn "Warning: Failed to persist event: #{e.message}"
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Create a short summary for compact log displays.
|
|
96
|
+
def summarize(message, max_length = 200)
|
|
78
97
|
str = case message
|
|
79
98
|
when Hash
|
|
80
99
|
message.to_json
|
|
@@ -84,7 +103,7 @@ module PromptObjects
|
|
|
84
103
|
message.to_s
|
|
85
104
|
end
|
|
86
105
|
|
|
87
|
-
#
|
|
106
|
+
# Collapse whitespace for single-line display
|
|
88
107
|
str = str.gsub(/\s+/, " ").strip
|
|
89
108
|
|
|
90
109
|
if str.length > max_length
|
|
@@ -63,7 +63,7 @@ module PromptObjects
|
|
|
63
63
|
content = normalize_message(message)
|
|
64
64
|
|
|
65
65
|
# Track who sent this message - another PO or a human?
|
|
66
|
-
sender = context.
|
|
66
|
+
sender = context.calling_po
|
|
67
67
|
from = (sender && sender != name) ? sender : "human"
|
|
68
68
|
|
|
69
69
|
user_msg = { role: :user, content: content, from: from }
|
|
@@ -88,7 +88,8 @@ module PromptObjects
|
|
|
88
88
|
# wait for tool results before generating a response. This prevents
|
|
89
89
|
# the model from "hedging" by generating both a response AND a tool call.
|
|
90
90
|
content: nil,
|
|
91
|
-
tool_calls: response.tool_calls
|
|
91
|
+
tool_calls: response.tool_calls,
|
|
92
|
+
usage: response.usage
|
|
92
93
|
}
|
|
93
94
|
@history << assistant_msg
|
|
94
95
|
persist_message(assistant_msg)
|
|
@@ -101,7 +102,7 @@ module PromptObjects
|
|
|
101
102
|
notify_history_updated
|
|
102
103
|
else
|
|
103
104
|
# No tool calls - we have our final response
|
|
104
|
-
assistant_msg = { role: :assistant, content: response.content }
|
|
105
|
+
assistant_msg = { role: :assistant, content: response.content, usage: response.usage }
|
|
105
106
|
@history << assistant_msg
|
|
106
107
|
persist_message(assistant_msg)
|
|
107
108
|
@state = :idle
|
|
@@ -390,7 +391,8 @@ module PromptObjects
|
|
|
390
391
|
session_id: @session_id,
|
|
391
392
|
role: :assistant,
|
|
392
393
|
content: msg[:content],
|
|
393
|
-
tool_calls: tool_calls_data
|
|
394
|
+
tool_calls: tool_calls_data,
|
|
395
|
+
usage: msg[:usage]
|
|
394
396
|
)
|
|
395
397
|
when :tool
|
|
396
398
|
session_store.add_message(
|
|
@@ -28,40 +28,53 @@ module PromptObjects
|
|
|
28
28
|
def route(request, path)
|
|
29
29
|
method = request.request_method
|
|
30
30
|
|
|
31
|
-
case
|
|
31
|
+
case method
|
|
32
|
+
when "GET"
|
|
33
|
+
route_get(request, path)
|
|
34
|
+
when "POST"
|
|
35
|
+
route_post(request, path)
|
|
36
|
+
else
|
|
37
|
+
{ error: "Not found", path: path }
|
|
38
|
+
end
|
|
39
|
+
end
|
|
32
40
|
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
def route_get(_request, path)
|
|
42
|
+
case path
|
|
43
|
+
when "/environment"
|
|
35
44
|
get_environment
|
|
36
|
-
|
|
37
|
-
# Prompt Objects
|
|
38
|
-
when ["GET", "/prompt_objects"]
|
|
45
|
+
when "/prompt_objects"
|
|
39
46
|
list_prompt_objects
|
|
40
|
-
|
|
41
|
-
when ["GET", %r{^/prompt_objects/([^/]+)$}]
|
|
42
|
-
get_prompt_object(path_param(path, 1))
|
|
43
|
-
|
|
44
|
-
when ["POST", "/prompt_objects"]
|
|
45
|
-
create_prompt_object(request.body.read)
|
|
46
|
-
|
|
47
|
-
# Sessions
|
|
48
|
-
when ["GET", %r{^/prompt_objects/([^/]+)/sessions$}]
|
|
49
|
-
list_sessions(path_param(path, 1))
|
|
50
|
-
|
|
51
|
-
when ["GET", %r{^/prompt_objects/([^/]+)/sessions/([^/]+)$}]
|
|
52
|
-
get_session(path_param(path, 1), path_param(path, 2))
|
|
53
|
-
|
|
54
|
-
when ["POST", %r{^/prompt_objects/([^/]+)/sessions$}]
|
|
55
|
-
create_session(path_param(path, 1), request.body.read)
|
|
56
|
-
|
|
57
|
-
# Primitives
|
|
58
|
-
when ["GET", "/primitives"]
|
|
47
|
+
when "/primitives"
|
|
59
48
|
list_primitives
|
|
60
|
-
|
|
61
|
-
# Message Bus
|
|
62
|
-
when ["GET", "/bus/recent"]
|
|
49
|
+
when "/bus/recent"
|
|
63
50
|
get_recent_bus_messages
|
|
51
|
+
when "/events"
|
|
52
|
+
get_recent_events(_request)
|
|
53
|
+
when %r{^/prompt_objects/([^/]+)/sessions/([^/]+)$}
|
|
54
|
+
get_session($1, $2)
|
|
55
|
+
when %r{^/prompt_objects/([^/]+)/sessions$}
|
|
56
|
+
list_sessions($1)
|
|
57
|
+
when %r{^/prompt_objects/([^/]+)$}
|
|
58
|
+
get_prompt_object($1)
|
|
59
|
+
when %r{^/events/session/([^/]+)$}
|
|
60
|
+
get_session_events($1)
|
|
61
|
+
when %r{^/sessions/([^/]+)/usage$}
|
|
62
|
+
get_session_usage($1, _request)
|
|
63
|
+
when %r{^/sessions/([^/]+)/export$}
|
|
64
|
+
export_thread($1, _request)
|
|
65
|
+
else
|
|
66
|
+
{ error: "Not found", path: path }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
64
69
|
|
|
70
|
+
def route_post(request, path)
|
|
71
|
+
case path
|
|
72
|
+
when "/prompt_objects"
|
|
73
|
+
create_prompt_object(request.body.read)
|
|
74
|
+
when %r{^/prompt_objects/([^/]+)/message$}
|
|
75
|
+
send_message($1, request.body.read)
|
|
76
|
+
when %r{^/prompt_objects/([^/]+)/sessions$}
|
|
77
|
+
create_session($1, request.body.read)
|
|
65
78
|
else
|
|
66
79
|
{ error: "Not found", path: path }
|
|
67
80
|
end
|
|
@@ -114,6 +127,61 @@ module PromptObjects
|
|
|
114
127
|
{ error: "Not implemented" }
|
|
115
128
|
end
|
|
116
129
|
|
|
130
|
+
# === Messages ===
|
|
131
|
+
|
|
132
|
+
def send_message(po_name, body)
|
|
133
|
+
po = @runtime.registry.get(po_name)
|
|
134
|
+
|
|
135
|
+
unless po.is_a?(PromptObject)
|
|
136
|
+
return { error: "Prompt object not found", name: po_name }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
params = JSON.parse(body)
|
|
140
|
+
message = params["message"]
|
|
141
|
+
session_id = params["session_id"]
|
|
142
|
+
new_thread = params["new_thread"]
|
|
143
|
+
|
|
144
|
+
return { error: "Message is required" } unless message && !message.empty?
|
|
145
|
+
|
|
146
|
+
# Create a new thread if requested
|
|
147
|
+
if new_thread
|
|
148
|
+
session_id = po.new_thread
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Switch to specified session if provided
|
|
152
|
+
if session_id && session_id != po.session_id
|
|
153
|
+
po.switch_session(session_id)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
request_session_id = po.session_id
|
|
157
|
+
|
|
158
|
+
# Send the message through the same path as WebSocket
|
|
159
|
+
context = @runtime.context(tui_mode: false)
|
|
160
|
+
context.current_capability = "human"
|
|
161
|
+
|
|
162
|
+
# Log to bus
|
|
163
|
+
@runtime.bus.publish(from: "human", to: po.name, message: message, session_id: request_session_id)
|
|
164
|
+
|
|
165
|
+
response = po.receive(message, context: context)
|
|
166
|
+
|
|
167
|
+
# Log response to bus
|
|
168
|
+
@runtime.bus.publish(from: po.name, to: "human", message: response, session_id: request_session_id)
|
|
169
|
+
|
|
170
|
+
# Count events for this session
|
|
171
|
+
event_count = if @runtime.session_store
|
|
172
|
+
@runtime.session_store.get_events(session_id: request_session_id).length
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
{
|
|
176
|
+
response: response,
|
|
177
|
+
po_name: po.name,
|
|
178
|
+
session_id: request_session_id,
|
|
179
|
+
event_count: event_count
|
|
180
|
+
}
|
|
181
|
+
rescue JSON::ParserError
|
|
182
|
+
{ error: "Invalid JSON body" }
|
|
183
|
+
end
|
|
184
|
+
|
|
117
185
|
# === Sessions ===
|
|
118
186
|
|
|
119
187
|
def list_sessions(po_name)
|
|
@@ -204,7 +272,8 @@ module PromptObjects
|
|
|
204
272
|
{
|
|
205
273
|
from: e[:from],
|
|
206
274
|
to: e[:to],
|
|
207
|
-
|
|
275
|
+
summary: e[:summary],
|
|
276
|
+
content: serialize_bus_content(e[:message]),
|
|
208
277
|
timestamp: e[:timestamp].iso8601
|
|
209
278
|
}
|
|
210
279
|
end
|
|
@@ -212,6 +281,94 @@ module PromptObjects
|
|
|
212
281
|
{ messages: messages }
|
|
213
282
|
end
|
|
214
283
|
|
|
284
|
+
def serialize_bus_content(message)
|
|
285
|
+
case message
|
|
286
|
+
when Hash then message
|
|
287
|
+
when String then message
|
|
288
|
+
else message.to_s
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
# === Events ===
|
|
293
|
+
|
|
294
|
+
def get_recent_events(request)
|
|
295
|
+
return { error: "No session store" } unless @runtime.session_store
|
|
296
|
+
|
|
297
|
+
count = (request.params["count"] || 50).to_i
|
|
298
|
+
events = @runtime.session_store.get_recent_events(count)
|
|
299
|
+
|
|
300
|
+
{ events: events.map { |e| format_event(e) } }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
def get_session_events(session_id)
|
|
304
|
+
return { error: "No session store" } unless @runtime.session_store
|
|
305
|
+
|
|
306
|
+
events = @runtime.session_store.get_events(session_id: session_id)
|
|
307
|
+
|
|
308
|
+
{ events: events.map { |e| format_event(e) } }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def format_event(event)
|
|
312
|
+
{
|
|
313
|
+
id: event[:id],
|
|
314
|
+
session_id: event[:session_id],
|
|
315
|
+
from: event[:from],
|
|
316
|
+
to: event[:to],
|
|
317
|
+
summary: event[:summary],
|
|
318
|
+
message: event[:message],
|
|
319
|
+
timestamp: event[:timestamp]&.iso8601
|
|
320
|
+
}
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
# === Usage ===
|
|
324
|
+
|
|
325
|
+
def get_session_usage(session_id, request)
|
|
326
|
+
return { error: "No session store" } unless @runtime.session_store
|
|
327
|
+
|
|
328
|
+
include_tree = request.params["tree"] == "true"
|
|
329
|
+
|
|
330
|
+
usage = if include_tree
|
|
331
|
+
@runtime.session_store.thread_tree_usage(session_id)
|
|
332
|
+
else
|
|
333
|
+
@runtime.session_store.session_usage(session_id)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
by_model = {}
|
|
337
|
+
usage[:by_model].each { |model, data| by_model[model.to_s] = data }
|
|
338
|
+
|
|
339
|
+
{
|
|
340
|
+
session_id: session_id,
|
|
341
|
+
include_tree: include_tree,
|
|
342
|
+
input_tokens: usage[:input_tokens],
|
|
343
|
+
output_tokens: usage[:output_tokens],
|
|
344
|
+
total_tokens: usage[:total_tokens],
|
|
345
|
+
estimated_cost_usd: usage[:estimated_cost_usd].round(6),
|
|
346
|
+
calls: usage[:calls],
|
|
347
|
+
by_model: by_model
|
|
348
|
+
}
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# === Export ===
|
|
352
|
+
|
|
353
|
+
def export_thread(session_id, request)
|
|
354
|
+
return { error: "No session store" } unless @runtime.session_store
|
|
355
|
+
|
|
356
|
+
format = request.params["format"] || "markdown"
|
|
357
|
+
|
|
358
|
+
case format
|
|
359
|
+
when "markdown"
|
|
360
|
+
content = @runtime.session_store.export_thread_tree_markdown(session_id)
|
|
361
|
+
return { error: "Session not found" } unless content
|
|
362
|
+
{ format: "markdown", content: content }
|
|
363
|
+
when "json"
|
|
364
|
+
data = @runtime.session_store.export_thread_tree_json(session_id)
|
|
365
|
+
return { error: "Session not found" } unless data
|
|
366
|
+
data
|
|
367
|
+
else
|
|
368
|
+
{ error: "Unknown format: #{format}" }
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
215
372
|
# === Helpers ===
|
|
216
373
|
|
|
217
374
|
def po_summary(po)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{top:0;right:0;bottom:0;left:0}.-right-1{right:-.25rem}.-top-1{top:-.25rem}.bottom-4{bottom:1rem}.left-2{left:.5rem}.right-0{right:0}.right-4{right:1rem}.top-0{top:0}.top-16{top:4rem}.top-full{top:100%}.z-10{z-index:10}.z-50{z-index:50}.my-3{margin-top:.75rem;margin-bottom:.75rem}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-px{margin-bottom:-1px}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.ml-1{margin-left:.25rem}.ml-11{margin-left:2.75rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-auto{margin-left:auto}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.line-clamp-2{overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-1\.5{height:.375rem}.h-14{height:3.5rem}.h-2{height:.5rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-8{height:2rem}.h-96{height:24rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-64{max-height:16rem}.max-h-\[60vh\]{max-height:60vh}.max-h-\[80vh\]{max-height:80vh}.w-1\.5{width:.375rem}.w-2{width:.5rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-56{width:14rem}.w-64{width:16rem}.w-8{width:2rem}.w-80{width:20rem}.w-96{width:24rem}.w-\[420px\]{width:420px}.w-full{width:100%}.min-w-\[160px\]{min-width:160px}.min-w-full{min-width:100%}.max-w-\[80\%\]{max-width:80%}.max-w-none{max-width:none}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}.rotate-180{--tw-rotate: 180deg;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@keyframes bounce{0%,to{transform:translateY(-25%);animation-timing-function:cubic-bezier(.8,0,1,1)}50%{transform:none;animation-timing-function:cubic-bezier(0,0,.2,1)}}.animate-bounce{animation:bounce 1s infinite}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize-none{resize:none}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.list-disc{list-style-type:disc}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-b-0{border-bottom-width:0px}.border-b-2{border-bottom-width:2px}.border-l{border-left-width:1px}.border-l-4{border-left-width:4px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-po-accent{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.border-po-border{--tw-border-opacity: 1;border-color:rgb(45 45 68 / var(--tw-border-opacity, 1))}.border-po-border\/30{border-color:#2d2d444d}.border-po-border\/50{border-color:#2d2d4480}.border-po-warning\/30{border-color:#f59e0b4d}.border-transparent{border-color:transparent}.bg-black\/50{background-color:#00000080}.bg-blue-600\/30{background-color:#2563eb4d}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-600\/30{background-color:#4b55634d}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-po-accent{--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity, 1))}.bg-po-accent\/20{background-color:#7c3aed33}.bg-po-bg{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}.bg-po-bg\/50{background-color:#0f0f1a80}.bg-po-bg\/80{background-color:#0f0f1acc}.bg-po-border{--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}.bg-po-surface{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.bg-po-surface\/50{background-color:#1a1a2e80}.bg-po-warning{--tw-bg-opacity: 1;background-color:rgb(245 158 11 / var(--tw-bg-opacity, 1))}.bg-po-warning\/10{background-color:#f59e0b1a}.bg-purple-600\/30{background-color:#9333ea4d}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-transparent{background-color:transparent}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pb-1{padding-bottom:.25rem}.pb-2{padding-bottom:.5rem}.pl-4{padding-left:1rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-\[10px\]{font-size:10px}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.normal-case{text-transform:none}.italic{font-style:italic}.leading-relaxed{line-height:1.625}.tracking-wide{letter-spacing:.025em}.tracking-wider{letter-spacing:.05em}.text-black{--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}.text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-po-accent{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.text-po-accent\/70{color:#7c3aedb3}.text-po-warning{--tw-text-opacity: 1;color:rgb(245 158 11 / var(--tw-text-opacity, 1))}.text-purple-300{--tw-text-opacity: 1;color:rgb(216 180 254 / var(--tw-text-opacity, 1))}.text-purple-400{--tw-text-opacity: 1;color:rgb(192 132 252 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.placeholder-gray-500::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-500::placeholder{--tw-placeholder-opacity: 1;color:rgb(107 114 128 / var(--tw-placeholder-opacity, 1))}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow: 0 20px 25px -5px rgb(0 0 0 / .1), 0 8px 10px -6px rgb(0 0 0 / .1);--tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}html{color-scheme:dark}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(124 58 237 / var(--tw-bg-opacity, 1))}.first\:mt-0:first-child{margin-top:0}.last\:mb-0:last-child{margin-bottom:0}.last\:border-0:last-child{border-width:0px}.hover\:border-po-accent:hover{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.hover\:border-po-accent\/50:hover{border-color:#7c3aed80}.hover\:bg-po-accent\/50:hover{background-color:#7c3aed80}.hover\:bg-po-accent\/80:hover{background-color:#7c3aedcc}.hover\:bg-po-bg:hover{--tw-bg-opacity: 1;background-color:rgb(15 15 26 / var(--tw-bg-opacity, 1))}.hover\:bg-po-bg\/70:hover{background-color:#0f0f1ab3}.hover\:bg-po-border:hover{--tw-bg-opacity: 1;background-color:rgb(45 45 68 / var(--tw-bg-opacity, 1))}.hover\:bg-po-surface:hover{--tw-bg-opacity: 1;background-color:rgb(26 26 46 / var(--tw-bg-opacity, 1))}.hover\:bg-po-warning\/20:hover{background-color:#f59e0b33}.hover\:text-po-accent:hover{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}.hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.focus\:border-po-accent:focus{--tw-border-opacity: 1;border-color:rgb(124 58 237 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-1:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-po-accent:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(124 58 237 / var(--tw-ring-opacity, 1))}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}.group:hover .group-hover\:text-po-accent{--tw-text-opacity: 1;color:rgb(124 58 237 / var(--tw-text-opacity, 1))}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:1280px){.xl\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}
|