prompt_objects 0.2.0 → 0.3.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/CHANGELOG.md +68 -0
- data/Gemfile.lock +1 -1
- data/README.md +2 -2
- data/exe/prompt_objects +387 -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 +5 -3
- 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
- metadata +20 -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
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 676489da9fefa02b4d187f99137469c6cc0011d99e7e43b72574c0d57b19c8a7
|
|
4
|
+
data.tar.gz: acd7c61e837ac00485cb8286cf95caa3d2103c82d07aa5fef7ce49185a3a0f6a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dda01aa39c36088e3433894b004994b4a24b735bd6b59bb6ae510943be0f6f83931d1e45fa34051b4f49a7243b606b7ac71b8790f8e5701f9652373dff29d756
|
|
7
|
+
data.tar.gz: e02fb7e206ced06445391d81012a61d790277f4fc0f46343e3e0391f46da58aafd3a047abd13147e0c2e8fe150284b5333759f27ab1037ccb6a7e8020d18316a
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to PromptObjects are documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.3.0] - 2025-02-05
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Token usage & cost tracking** — Track input/output tokens and estimated costs per session and across delegation trees. Includes per-model pricing table and a Usage Panel in the web UI (right-click a thread to view).
|
|
10
|
+
- **Ollama & OpenRouter support** — Connect to local Ollama models or OpenRouter's model marketplace. Both reuse the OpenAI adapter with configurable base URLs. Ollama models are auto-discovered from the local API.
|
|
11
|
+
- **Thread export** — Export any conversation thread as Markdown or JSON, including full delegation chains. Delegation sub-threads render inline next to the tool call that triggered them, preserving the actual flow of work. Available via right-click context menu or REST API.
|
|
12
|
+
- **ARC-AGI-1 template** — A template for solving ARC-AGI challenges with a solver PO, data manager PO, and 7 custom grid primitives (load, render, diff, info, find objects, transform, test solution).
|
|
13
|
+
- **Persistent event log** — Message bus events are now persisted to SQLite for replay and debugging.
|
|
14
|
+
- **REST message endpoint & events API** — Send messages to POs and retrieve bus events via HTTP. Includes server discovery for CLI commands.
|
|
15
|
+
- **CLI `message` and `events` commands** — Interact with a running environment from the command line without opening the web UI.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- Custom primitives (created by POs or from templates) now auto-load on environment startup. Previously they were saved to `env/primitives/` but never registered on restart.
|
|
20
|
+
- Message serialization bugs that caused crashes when tool calls contained non-string values.
|
|
21
|
+
- Frontend auto-rebuilds when running `prompt_objects serve` in development.
|
|
22
|
+
- Delegation sub-threads in exports now appear inline after the triggering tool call, not at the bottom of the document.
|
|
23
|
+
- Tool result truncation limit increased from 2,000 to 10,000 characters to preserve detail in exports.
|
|
24
|
+
- Full message content stored in bus; truncation applied only at display time.
|
|
25
|
+
|
|
26
|
+
## [0.2.0] - 2025-01-23
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **GitHub Actions CI** — Automated test suite running on Ruby 3.2, 3.3, and 3.4.
|
|
31
|
+
- **Conversation threads with delegation isolation** — Each PO-to-PO delegation runs in its own thread, keeping conversations clean.
|
|
32
|
+
- **Thread sidebar** — Navigate between threads with auto-naming and instant feedback on creation.
|
|
33
|
+
- **Real-time capability updates** — Adding/removing capabilities broadcasts changes to the web UI immediately.
|
|
34
|
+
- **PO prompt editing** — Edit a Prompt Object's system prompt directly in the web UI with auto-save back to the markdown file.
|
|
35
|
+
- **`modify_prompt` universal capability** — POs can rewrite their own system prompts at runtime.
|
|
36
|
+
- **Environment recovery tools** — `remove_capability` and `delete_primitive` for cleaning up broken state.
|
|
37
|
+
- **Streaming tool calls** — Tool call chains display in real-time as they execute, not just after completion.
|
|
38
|
+
- **Capabilities panel** — Visual display of each PO's available primitives and PO-to-PO capabilities.
|
|
39
|
+
- **Core PromptObject tests** — Unit test suite for the core framework.
|
|
40
|
+
|
|
41
|
+
### Fixed
|
|
42
|
+
|
|
43
|
+
- Session store binding and Gemini model name resolution.
|
|
44
|
+
- Tool results missing function name for Gemini API compatibility.
|
|
45
|
+
- `tool_calls` Hash vs ToolCall object handling across all adapters.
|
|
46
|
+
- Claude API response parsing for new PO chat updates.
|
|
47
|
+
- Thread switching now immediately shows the new thread on creation.
|
|
48
|
+
- Session message counts and cross-session response routing.
|
|
49
|
+
|
|
50
|
+
## [0.1.0] - 2025-01-15
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **Core framework** — Markdown files with YAML frontmatter act as LLM-backed autonomous entities.
|
|
55
|
+
- **Unified capability interface** — Primitives (Ruby) and Prompt Objects (Markdown) share the same `receive(message, context:)` interface.
|
|
56
|
+
- **Built-in primitives** — `read_file`, `list_files`, `write_file`, `http_get`.
|
|
57
|
+
- **Universal capabilities** — `ask_human`, `think`, `request_capability`, `create_capability`, `add_capability`.
|
|
58
|
+
- **Multi-provider LLM support** — OpenAI, Anthropic, and Gemini adapters with model selection UI.
|
|
59
|
+
- **PO-to-PO communication** — Prompt Objects can call each other as capabilities through the message bus.
|
|
60
|
+
- **Self-modification** — POs can create new Prompt Objects and primitives at runtime (with human approval).
|
|
61
|
+
- **Web UI** — React frontend with real-time WebSocket updates, split-view layout, markdown rendering.
|
|
62
|
+
- **Notification system** — Non-blocking human-in-the-loop via `ask_human` with notification bell and dropdown.
|
|
63
|
+
- **Live filesystem watching** — Changes to `.md` files in the objects directory are reflected immediately.
|
|
64
|
+
- **SQLite session storage** — Persistent conversation history with WAL mode for concurrent access.
|
|
65
|
+
- **Environment management** — Create, list, and manage isolated environments with `prompt_objects env` commands.
|
|
66
|
+
- **Templates** — Bootstrap new environments from templates (`basic`, `pair`, `team`, and more).
|
|
67
|
+
- **MCP server mode** — Expose POs as tools via the Model Context Protocol for external client integration.
|
|
68
|
+
- **CLI** — `prompt_objects` command with subcommands for environment management, serving, and interaction.
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -45,8 +45,8 @@ gem install prompt_objects
|
|
|
45
45
|
# Create an environment from a template
|
|
46
46
|
prompt_objects env create my-project --template basic
|
|
47
47
|
|
|
48
|
-
# Run the web interface
|
|
49
|
-
prompt_objects
|
|
48
|
+
# Run and open the web interface
|
|
49
|
+
prompt_objects serve my-project --open
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
### Environment Commands
|
data/exe/prompt_objects
CHANGED
|
@@ -264,7 +264,7 @@ class REPL
|
|
|
264
264
|
time = entry[:timestamp].strftime("%H:%M:%S")
|
|
265
265
|
from = entry[:from]
|
|
266
266
|
to = entry[:to]
|
|
267
|
-
msg = entry[:
|
|
267
|
+
msg = entry[:summary]
|
|
268
268
|
|
|
269
269
|
line = "#{time} #{from} -> #{to}: #{msg}"
|
|
270
270
|
line = line[0, 70] + "..." if line.length > 73
|
|
@@ -283,6 +283,8 @@ def print_main_help
|
|
|
283
283
|
Commands:
|
|
284
284
|
env Manage environments (create, list, export, etc.)
|
|
285
285
|
serve <env> Run environment as a web server
|
|
286
|
+
message <env> <po> "text" Send a message to a PO and print the response
|
|
287
|
+
events <env> [--session ID] Show recent events from the message bus
|
|
286
288
|
repl [name] [objects_dir] Start interactive REPL with a prompt object
|
|
287
289
|
help Show this help message
|
|
288
290
|
|
|
@@ -293,6 +295,8 @@ def print_main_help
|
|
|
293
295
|
prompt_objects env create demo --template demo # Create from template
|
|
294
296
|
prompt_objects serve my-env # Start web UI
|
|
295
297
|
prompt_objects serve my-env --open # Start and open browser
|
|
298
|
+
prompt_objects message my-env solver "Hello" # Send a message
|
|
299
|
+
prompt_objects message my-env solver "Hello" --json # JSON output
|
|
296
300
|
HELP
|
|
297
301
|
end
|
|
298
302
|
|
|
@@ -476,6 +480,9 @@ def run_serve(args)
|
|
|
476
480
|
elsif options[:web]
|
|
477
481
|
require "prompt_objects/server"
|
|
478
482
|
|
|
483
|
+
# Rebuild frontend to ensure built assets match source
|
|
484
|
+
rebuild_frontend
|
|
485
|
+
|
|
479
486
|
# Open browser if requested
|
|
480
487
|
if options[:open]
|
|
481
488
|
url = "http://#{options[:host]}:#{options[:port]}"
|
|
@@ -491,6 +498,31 @@ def run_serve(args)
|
|
|
491
498
|
end
|
|
492
499
|
end
|
|
493
500
|
|
|
501
|
+
def rebuild_frontend
|
|
502
|
+
# Find the frontend directory relative to this executable
|
|
503
|
+
gem_root = File.expand_path("../..", __FILE__)
|
|
504
|
+
frontend_dir = File.join(gem_root, "frontend")
|
|
505
|
+
|
|
506
|
+
unless Dir.exist?(frontend_dir)
|
|
507
|
+
# No frontend source (e.g., installed as gem without source) — skip
|
|
508
|
+
return
|
|
509
|
+
end
|
|
510
|
+
|
|
511
|
+
node_modules = File.join(frontend_dir, "node_modules")
|
|
512
|
+
unless Dir.exist?(node_modules)
|
|
513
|
+
puts "Frontend dependencies not installed. Run: cd frontend && npm install"
|
|
514
|
+
return
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
print "Building frontend... "
|
|
518
|
+
result = system("npm run build --prefix #{frontend_dir}", out: File::NULL, err: File::NULL)
|
|
519
|
+
if result
|
|
520
|
+
puts "done"
|
|
521
|
+
else
|
|
522
|
+
puts "failed (serving stale assets)"
|
|
523
|
+
end
|
|
524
|
+
end
|
|
525
|
+
|
|
494
526
|
def open_browser(url)
|
|
495
527
|
case RUBY_PLATFORM
|
|
496
528
|
when /darwin/
|
|
@@ -538,6 +570,356 @@ def load_all_objects(runtime, env_path)
|
|
|
538
570
|
end
|
|
539
571
|
end
|
|
540
572
|
|
|
573
|
+
def print_message_help
|
|
574
|
+
puts <<~HELP
|
|
575
|
+
Usage: prompt_objects message [options] <environment> <po_name> "message"
|
|
576
|
+
|
|
577
|
+
Send a message to a prompt object and print the response.
|
|
578
|
+
|
|
579
|
+
Arguments:
|
|
580
|
+
environment Environment name or path
|
|
581
|
+
po_name Name of the prompt object to message
|
|
582
|
+
message The message to send (quoted string)
|
|
583
|
+
|
|
584
|
+
Options:
|
|
585
|
+
--json Output response as JSON
|
|
586
|
+
--events Also print the event log for this interaction
|
|
587
|
+
--new-thread Start a new thread for this message
|
|
588
|
+
--session ID Use a specific session ID
|
|
589
|
+
--help, -h Show this help
|
|
590
|
+
|
|
591
|
+
If a server is running for the environment, the message is sent via
|
|
592
|
+
HTTP to the running server (and streams to the web UI in real time).
|
|
593
|
+
Otherwise, a standalone runtime is started for the request.
|
|
594
|
+
|
|
595
|
+
Examples:
|
|
596
|
+
prompt_objects message my-env solver "What patterns do you see?"
|
|
597
|
+
prompt_objects message my-env solver "Solve task 1" --json
|
|
598
|
+
prompt_objects message my-env solver "Try again" --new-thread
|
|
599
|
+
HELP
|
|
600
|
+
end
|
|
601
|
+
|
|
602
|
+
def run_message(args)
|
|
603
|
+
options = {
|
|
604
|
+
json: false,
|
|
605
|
+
events: false,
|
|
606
|
+
new_thread: false,
|
|
607
|
+
session_id: nil
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
positional = []
|
|
611
|
+
skip_next = false
|
|
612
|
+
|
|
613
|
+
args.each_with_index do |arg, i|
|
|
614
|
+
if skip_next
|
|
615
|
+
skip_next = false
|
|
616
|
+
next
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
case arg
|
|
620
|
+
when "--json"
|
|
621
|
+
options[:json] = true
|
|
622
|
+
when "--events"
|
|
623
|
+
options[:events] = true
|
|
624
|
+
when "--new-thread"
|
|
625
|
+
options[:new_thread] = true
|
|
626
|
+
when "--session"
|
|
627
|
+
options[:session_id] = args[i + 1]
|
|
628
|
+
skip_next = true
|
|
629
|
+
when "--help", "-h"
|
|
630
|
+
print_message_help
|
|
631
|
+
exit 0
|
|
632
|
+
else
|
|
633
|
+
positional << arg
|
|
634
|
+
end
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
env_name = positional[0]
|
|
638
|
+
po_name = positional[1]
|
|
639
|
+
message_text = positional[2]
|
|
640
|
+
|
|
641
|
+
unless env_name && po_name && message_text
|
|
642
|
+
puts "Error: environment, po_name, and message are required"
|
|
643
|
+
puts "Run 'prompt_objects message --help' for usage"
|
|
644
|
+
exit 1
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# Resolve environment
|
|
648
|
+
env_path = resolve_environment(env_name)
|
|
649
|
+
unless env_path
|
|
650
|
+
$stderr.puts "Error: environment '#{env_name}' not found"
|
|
651
|
+
exit 1
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
# Check for running server
|
|
655
|
+
require "prompt_objects/server"
|
|
656
|
+
server_info = PromptObjects::Server.read_server_file(env_path)
|
|
657
|
+
|
|
658
|
+
if server_info
|
|
659
|
+
# Send via HTTP to running server
|
|
660
|
+
send_message_via_http(server_info, po_name, message_text, options)
|
|
661
|
+
else
|
|
662
|
+
# Standalone mode
|
|
663
|
+
send_message_standalone(env_path, po_name, message_text, options)
|
|
664
|
+
end
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def send_message_via_http(server_info, po_name, message_text, options)
|
|
668
|
+
require "net/http"
|
|
669
|
+
require "uri"
|
|
670
|
+
|
|
671
|
+
uri = URI("http://#{server_info[:host]}:#{server_info[:port]}/api/prompt_objects/#{po_name}/message")
|
|
672
|
+
|
|
673
|
+
body = { message: message_text }
|
|
674
|
+
body[:new_thread] = true if options[:new_thread]
|
|
675
|
+
body[:session_id] = options[:session_id] if options[:session_id]
|
|
676
|
+
|
|
677
|
+
begin
|
|
678
|
+
response = Net::HTTP.post(uri, body.to_json, "Content-Type" => "application/json")
|
|
679
|
+
data = JSON.parse(response.body)
|
|
680
|
+
|
|
681
|
+
if data["error"]
|
|
682
|
+
$stderr.puts "Error: #{data['error']}"
|
|
683
|
+
exit 1
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
if options[:json]
|
|
687
|
+
puts JSON.pretty_generate(data)
|
|
688
|
+
else
|
|
689
|
+
puts data["response"]
|
|
690
|
+
end
|
|
691
|
+
|
|
692
|
+
if options[:events] && data["session_id"]
|
|
693
|
+
events_uri = URI("http://#{server_info[:host]}:#{server_info[:port]}/api/events/session/#{data['session_id']}")
|
|
694
|
+
events_response = Net::HTTP.get(events_uri)
|
|
695
|
+
events_data = JSON.parse(events_response)
|
|
696
|
+
|
|
697
|
+
if events_data["events"]&.any?
|
|
698
|
+
$stderr.puts
|
|
699
|
+
$stderr.puts "--- Events ---"
|
|
700
|
+
events_data["events"].each do |e|
|
|
701
|
+
$stderr.puts "#{e['timestamp']} #{e['from']} -> #{e['to']}: #{e['summary']}"
|
|
702
|
+
end
|
|
703
|
+
$stderr.puts "--------------"
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
rescue Errno::ECONNREFUSED
|
|
707
|
+
$stderr.puts "Error: Server at #{server_info[:host]}:#{server_info[:port]} is not responding"
|
|
708
|
+
$stderr.puts "The .server file may be stale. Try running standalone."
|
|
709
|
+
exit 1
|
|
710
|
+
end
|
|
711
|
+
end
|
|
712
|
+
|
|
713
|
+
def send_message_standalone(env_path, po_name, message_text, options)
|
|
714
|
+
runtime = PromptObjects::Runtime.new(env_path: env_path)
|
|
715
|
+
load_all_objects(runtime, env_path)
|
|
716
|
+
|
|
717
|
+
po = runtime.registry.get(po_name)
|
|
718
|
+
unless po.is_a?(PromptObjects::PromptObject)
|
|
719
|
+
$stderr.puts "Error: prompt object '#{po_name}' not found"
|
|
720
|
+
$stderr.puts "Available: #{runtime.loaded_objects.join(', ')}"
|
|
721
|
+
exit 1
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# Create new thread if requested
|
|
725
|
+
po.new_thread if options[:new_thread]
|
|
726
|
+
|
|
727
|
+
# Switch session if specified
|
|
728
|
+
if options[:session_id]
|
|
729
|
+
po.switch_session(options[:session_id])
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
session_id = po.session_id
|
|
733
|
+
log_start = runtime.bus.log.length
|
|
734
|
+
|
|
735
|
+
context = runtime.context
|
|
736
|
+
context.current_capability = "human"
|
|
737
|
+
|
|
738
|
+
# Log to bus
|
|
739
|
+
runtime.bus.publish(from: "human", to: po.name, message: message_text, session_id: session_id)
|
|
740
|
+
|
|
741
|
+
response = po.receive(message_text, context: context)
|
|
742
|
+
|
|
743
|
+
# Log response to bus
|
|
744
|
+
runtime.bus.publish(from: po.name, to: "human", message: response, session_id: session_id)
|
|
745
|
+
|
|
746
|
+
if options[:json]
|
|
747
|
+
data = {
|
|
748
|
+
response: response,
|
|
749
|
+
po_name: po.name,
|
|
750
|
+
session_id: session_id,
|
|
751
|
+
event_count: runtime.bus.log.length - log_start
|
|
752
|
+
}
|
|
753
|
+
puts JSON.pretty_generate(data)
|
|
754
|
+
else
|
|
755
|
+
puts response
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
if options[:events]
|
|
759
|
+
entries = runtime.bus.log[log_start..]
|
|
760
|
+
if entries&.any?
|
|
761
|
+
$stderr.puts
|
|
762
|
+
$stderr.puts "--- Events ---"
|
|
763
|
+
entries.each do |entry|
|
|
764
|
+
time = entry[:timestamp].strftime("%H:%M:%S")
|
|
765
|
+
$stderr.puts "#{time} #{entry[:from]} -> #{entry[:to]}: #{entry[:summary]}"
|
|
766
|
+
end
|
|
767
|
+
$stderr.puts "--------------"
|
|
768
|
+
end
|
|
769
|
+
end
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def print_events_help
|
|
773
|
+
puts <<~HELP
|
|
774
|
+
Usage: prompt_objects events [options] <environment>
|
|
775
|
+
|
|
776
|
+
Show recent events from the message bus.
|
|
777
|
+
|
|
778
|
+
Arguments:
|
|
779
|
+
environment Environment name or path
|
|
780
|
+
|
|
781
|
+
Options:
|
|
782
|
+
--session ID Show events for a specific session
|
|
783
|
+
--count N Number of events to show (default: 50)
|
|
784
|
+
--json Output as JSON
|
|
785
|
+
--help, -h Show this help
|
|
786
|
+
|
|
787
|
+
Examples:
|
|
788
|
+
prompt_objects events my-env
|
|
789
|
+
prompt_objects events my-env --session abc-123
|
|
790
|
+
prompt_objects events my-env --count 100 --json
|
|
791
|
+
HELP
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def run_events(args)
|
|
795
|
+
options = { count: 50, session_id: nil, json: false }
|
|
796
|
+
positional = []
|
|
797
|
+
skip_next = false
|
|
798
|
+
|
|
799
|
+
args.each_with_index do |arg, i|
|
|
800
|
+
if skip_next
|
|
801
|
+
skip_next = false
|
|
802
|
+
next
|
|
803
|
+
end
|
|
804
|
+
|
|
805
|
+
case arg
|
|
806
|
+
when "--session"
|
|
807
|
+
options[:session_id] = args[i + 1]
|
|
808
|
+
skip_next = true
|
|
809
|
+
when "--count"
|
|
810
|
+
options[:count] = args[i + 1].to_i
|
|
811
|
+
skip_next = true
|
|
812
|
+
when "--json"
|
|
813
|
+
options[:json] = true
|
|
814
|
+
when "--help", "-h"
|
|
815
|
+
print_events_help
|
|
816
|
+
exit 0
|
|
817
|
+
else
|
|
818
|
+
positional << arg
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
env_name = positional[0]
|
|
823
|
+
unless env_name
|
|
824
|
+
puts "Error: environment name required"
|
|
825
|
+
puts "Run 'prompt_objects events --help' for usage"
|
|
826
|
+
exit 1
|
|
827
|
+
end
|
|
828
|
+
|
|
829
|
+
env_path = resolve_environment(env_name)
|
|
830
|
+
unless env_path
|
|
831
|
+
$stderr.puts "Error: environment '#{env_name}' not found"
|
|
832
|
+
exit 1
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
# Check for running server
|
|
836
|
+
require "prompt_objects/server"
|
|
837
|
+
server_info = PromptObjects::Server.read_server_file(env_path)
|
|
838
|
+
|
|
839
|
+
if server_info
|
|
840
|
+
show_events_via_http(server_info, options)
|
|
841
|
+
else
|
|
842
|
+
show_events_standalone(env_path, options)
|
|
843
|
+
end
|
|
844
|
+
end
|
|
845
|
+
|
|
846
|
+
def show_events_via_http(server_info, options)
|
|
847
|
+
require "net/http"
|
|
848
|
+
require "uri"
|
|
849
|
+
|
|
850
|
+
if options[:session_id]
|
|
851
|
+
uri = URI("http://#{server_info[:host]}:#{server_info[:port]}/api/events/session/#{options[:session_id]}")
|
|
852
|
+
else
|
|
853
|
+
uri = URI("http://#{server_info[:host]}:#{server_info[:port]}/api/events?count=#{options[:count]}")
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
response = Net::HTTP.get(uri)
|
|
857
|
+
data = JSON.parse(response)
|
|
858
|
+
|
|
859
|
+
if options[:json]
|
|
860
|
+
puts JSON.pretty_generate(data)
|
|
861
|
+
else
|
|
862
|
+
print_events(data["events"] || [])
|
|
863
|
+
end
|
|
864
|
+
rescue Errno::ECONNREFUSED
|
|
865
|
+
$stderr.puts "Error: Server not responding"
|
|
866
|
+
exit 1
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
def show_events_standalone(env_path, options)
|
|
870
|
+
db_path = File.join(env_path, "sessions.db")
|
|
871
|
+
unless File.exist?(db_path)
|
|
872
|
+
puts "No event history (sessions.db not found)"
|
|
873
|
+
exit 0
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
store = PromptObjects::Session::Store.new(db_path)
|
|
877
|
+
|
|
878
|
+
events = if options[:session_id]
|
|
879
|
+
store.get_events(session_id: options[:session_id])
|
|
880
|
+
else
|
|
881
|
+
store.get_recent_events(options[:count])
|
|
882
|
+
end
|
|
883
|
+
|
|
884
|
+
if options[:json]
|
|
885
|
+
formatted = events.map do |e|
|
|
886
|
+
{
|
|
887
|
+
id: e[:id],
|
|
888
|
+
from: e[:from],
|
|
889
|
+
to: e[:to],
|
|
890
|
+
summary: e[:summary],
|
|
891
|
+
message: e[:message],
|
|
892
|
+
timestamp: e[:timestamp]&.iso8601,
|
|
893
|
+
session_id: e[:session_id]
|
|
894
|
+
}
|
|
895
|
+
end
|
|
896
|
+
puts JSON.pretty_generate(formatted)
|
|
897
|
+
else
|
|
898
|
+
print_events(events)
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
store.close
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
def print_events(events)
|
|
905
|
+
if events.empty?
|
|
906
|
+
puts "No events found."
|
|
907
|
+
return
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
events.each do |e|
|
|
911
|
+
ts = e[:timestamp] || e["timestamp"]
|
|
912
|
+
ts = ts.is_a?(Time) ? ts.strftime("%H:%M:%S") : ts&.split("T")&.last&.split(".")&.first
|
|
913
|
+
from = e[:from] || e["from"]
|
|
914
|
+
to = e[:to] || e["to"]
|
|
915
|
+
summary = e[:summary] || e["summary"]
|
|
916
|
+
|
|
917
|
+
line = "#{ts} #{from} -> #{to}: #{summary}"
|
|
918
|
+
line = line[0, 120] + "..." if line.length > 123
|
|
919
|
+
puts line
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
|
|
541
923
|
# === Main Entry Point ===
|
|
542
924
|
|
|
543
925
|
def run_env(args)
|
|
@@ -561,6 +943,10 @@ def main
|
|
|
561
943
|
run_repl(args)
|
|
562
944
|
when "serve"
|
|
563
945
|
run_serve(args)
|
|
946
|
+
when "message", "msg"
|
|
947
|
+
run_message(args)
|
|
948
|
+
when "events"
|
|
949
|
+
run_events(args)
|
|
564
950
|
when "help", "--help", "-h"
|
|
565
951
|
print_main_help
|
|
566
952
|
else
|
data/frontend/src/App.tsx
CHANGED
|
@@ -7,11 +7,12 @@ import { PODetail } from './components/PODetail'
|
|
|
7
7
|
import { MessageBus } from './components/MessageBus'
|
|
8
8
|
import { NotificationPanel } from './components/NotificationPanel'
|
|
9
9
|
import { ThreadsSidebar } from './components/ThreadsSidebar'
|
|
10
|
+
import { UsagePanel } from './components/UsagePanel'
|
|
10
11
|
|
|
11
12
|
export default function App() {
|
|
12
|
-
const { sendMessage, respondToNotification, createSession, switchSession, switchLLM, createThread, updatePrompt } =
|
|
13
|
+
const { sendMessage, respondToNotification, createSession, switchSession, switchLLM, createThread, updatePrompt, requestUsage, exportThread } =
|
|
13
14
|
useWebSocket()
|
|
14
|
-
const { selectedPO, busOpen, notifications } = useStore()
|
|
15
|
+
const { selectedPO, busOpen, notifications, usageData, clearUsageData } = useStore()
|
|
15
16
|
const selectedPOData = useSelectedPO()
|
|
16
17
|
const [splitView, setSplitView] = useState(true) // Default to split view
|
|
17
18
|
|
|
@@ -47,6 +48,8 @@ export default function App() {
|
|
|
47
48
|
po={selectedPOData}
|
|
48
49
|
switchSession={switchSession}
|
|
49
50
|
createThread={createThread}
|
|
51
|
+
requestUsage={requestUsage}
|
|
52
|
+
exportThread={exportThread}
|
|
50
53
|
/>
|
|
51
54
|
</aside>
|
|
52
55
|
)}
|
|
@@ -81,7 +84,7 @@ export default function App() {
|
|
|
81
84
|
|
|
82
85
|
{/* Message Bus sidebar */}
|
|
83
86
|
{busOpen && (
|
|
84
|
-
<aside className="w-80 border-l border-po-border bg-po-surface overflow-hidden">
|
|
87
|
+
<aside className="w-80 flex-shrink-0 border-l border-po-border bg-po-surface overflow-hidden">
|
|
85
88
|
<MessageBus />
|
|
86
89
|
</aside>
|
|
87
90
|
)}
|
|
@@ -91,6 +94,11 @@ export default function App() {
|
|
|
91
94
|
{notifications.length > 0 && (
|
|
92
95
|
<NotificationPanel respondToNotification={respondToNotification} />
|
|
93
96
|
)}
|
|
97
|
+
|
|
98
|
+
{/* Usage panel modal */}
|
|
99
|
+
{usageData && (
|
|
100
|
+
<UsagePanel usage={usageData as any} onClose={clearUsageData} />
|
|
101
|
+
)}
|
|
94
102
|
</div>
|
|
95
103
|
)
|
|
96
104
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react'
|
|
2
|
+
|
|
3
|
+
interface ContextMenuItem {
|
|
4
|
+
label: string
|
|
5
|
+
onClick: () => void
|
|
6
|
+
icon?: string
|
|
7
|
+
danger?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ContextMenuProps {
|
|
11
|
+
x: number
|
|
12
|
+
y: number
|
|
13
|
+
items: ContextMenuItem[]
|
|
14
|
+
onClose: () => void
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ContextMenu({ x, y, items, onClose }: ContextMenuProps) {
|
|
18
|
+
const menuRef = useRef<HTMLDivElement>(null)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const handleClickOutside = (e: MouseEvent) => {
|
|
22
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
23
|
+
onClose()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const handleEscape = (e: KeyboardEvent) => {
|
|
27
|
+
if (e.key === 'Escape') onClose()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
document.addEventListener('mousedown', handleClickOutside)
|
|
31
|
+
document.addEventListener('keydown', handleEscape)
|
|
32
|
+
return () => {
|
|
33
|
+
document.removeEventListener('mousedown', handleClickOutside)
|
|
34
|
+
document.removeEventListener('keydown', handleEscape)
|
|
35
|
+
}
|
|
36
|
+
}, [onClose])
|
|
37
|
+
|
|
38
|
+
// Adjust position to stay within viewport
|
|
39
|
+
const adjustedStyle = {
|
|
40
|
+
top: y,
|
|
41
|
+
left: x,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div
|
|
46
|
+
ref={menuRef}
|
|
47
|
+
className="fixed z-50 bg-po-surface border border-po-border rounded-lg shadow-xl py-1 min-w-[160px]"
|
|
48
|
+
style={adjustedStyle}
|
|
49
|
+
>
|
|
50
|
+
{items.map((item, idx) => (
|
|
51
|
+
<button
|
|
52
|
+
key={idx}
|
|
53
|
+
onClick={() => {
|
|
54
|
+
item.onClick()
|
|
55
|
+
onClose()
|
|
56
|
+
}}
|
|
57
|
+
className={`w-full text-left px-3 py-1.5 text-xs hover:bg-po-bg transition-colors flex items-center gap-2 ${
|
|
58
|
+
item.danger ? 'text-red-400 hover:text-red-300' : 'text-gray-300 hover:text-white'
|
|
59
|
+
}`}
|
|
60
|
+
>
|
|
61
|
+
{item.icon && <span>{item.icon}</span>}
|
|
62
|
+
{item.label}
|
|
63
|
+
</button>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -41,9 +41,10 @@ export function MessageBus() {
|
|
|
41
41
|
</span>
|
|
42
42
|
</div>
|
|
43
43
|
<div className="text-gray-300 break-words">
|
|
44
|
-
{
|
|
45
|
-
|
|
46
|
-
:
|
|
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
|
+
})()}
|
|
47
48
|
</div>
|
|
48
49
|
</div>
|
|
49
50
|
))
|
|
@@ -33,6 +33,8 @@ export function ModelSelector({ switchLLM }: Props) {
|
|
|
33
33
|
openai: 'OpenAI',
|
|
34
34
|
anthropic: 'Anthropic',
|
|
35
35
|
gemini: 'Gemini',
|
|
36
|
+
ollama: 'Ollama',
|
|
37
|
+
openrouter: 'OpenRouter',
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
return (
|
|
@@ -62,7 +64,9 @@ export function ModelSelector({ switchLLM }: Props) {
|
|
|
62
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
65
|
<span>{providerNames[provider.name] || provider.name}</span>
|
|
64
66
|
{!provider.available && (
|
|
65
|
-
<span className="text-red-400 text-[10px] normal-case">
|
|
67
|
+
<span className="text-red-400 text-[10px] normal-case">
|
|
68
|
+
{provider.name === 'ollama' ? 'Not Running' : 'No API Key'}
|
|
69
|
+
</span>
|
|
66
70
|
)}
|
|
67
71
|
</div>
|
|
68
72
|
{provider.models.map((model) => {
|