prompt_objects 0.3.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 +12 -0
- data/Gemfile.lock +1 -1
- data/exe/prompt_objects +161 -0
- data/lib/prompt_objects/prompt_object.rb +1 -1
- data/prompt_objects.gemspec +1 -1
- data/tools/thread-explorer.html +1043 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0a1ec32b66d34c2fe11c964759513487335794f9a8c336bd32f7ae9aeed5380c
|
|
4
|
+
data.tar.gz: 9a6650251228ab4dae25f16120e8ae8c1992e4458faf0c858894b6e4848ae80b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 867fc4cb49114ace4af4682feff5a062eb414f0ce84e4be4e48632609d68cec2ac201be2cb43fb995ac5e9cb4ec0480399aa6eaad7580da81fd5451905b0ddf5
|
|
7
|
+
data.tar.gz: 4b3d3c3d96d45a2573829345fd1f822fa3f8918c9d4088cccc46d82ba4fa797bb7c9838e74d443017069f7cd9b9d343d3bd92219930b12098f036d038e05ef83
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to PromptObjects are documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.3.1] - 2025-02-08
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Thread Explorer** — Standalone HTML visualizer for exploring conversation thread exports. Three views: sequence diagram (swim lanes showing PO communication), timeline (flat chronological event list), and detail panel (full conversation with tool calls). Includes token cost bar, search, per-PO filtering, breadcrumb navigation, and structural event highlighting.
|
|
10
|
+
- **`explore` CLI command** — Open Thread Explorer from the command line. `prompt_objects explore <env>` lists root threads and opens the most recent; `--session ID` targets a specific thread. Data is embedded directly so it opens ready to go.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Delegation messages now correctly show the calling PO's name instead of "human". Previously `context.current_capability` resolved to the target PO (matching its own name), causing a fallback to "human". Now uses `context.calling_po`.
|
|
15
|
+
- CLI integration tests skip gracefully in CI when no LLM API key is available, instead of failing.
|
|
16
|
+
|
|
5
17
|
## [0.3.0] - 2025-02-05
|
|
6
18
|
|
|
7
19
|
### Added
|
data/Gemfile.lock
CHANGED
data/exe/prompt_objects
CHANGED
|
@@ -285,6 +285,7 @@ def print_main_help
|
|
|
285
285
|
serve <env> Run environment as a web server
|
|
286
286
|
message <env> <po> "text" Send a message to a PO and print the response
|
|
287
287
|
events <env> [--session ID] Show recent events from the message bus
|
|
288
|
+
explore [env] [--session ID] Open Thread Explorer to visualize conversations
|
|
288
289
|
repl [name] [objects_dir] Start interactive REPL with a prompt object
|
|
289
290
|
help Show this help message
|
|
290
291
|
|
|
@@ -297,6 +298,7 @@ def print_main_help
|
|
|
297
298
|
prompt_objects serve my-env --open # Start and open browser
|
|
298
299
|
prompt_objects message my-env solver "Hello" # Send a message
|
|
299
300
|
prompt_objects message my-env solver "Hello" --json # JSON output
|
|
301
|
+
prompt_objects explore my-env # Visualize conversations
|
|
300
302
|
HELP
|
|
301
303
|
end
|
|
302
304
|
|
|
@@ -920,6 +922,163 @@ def print_events(events)
|
|
|
920
922
|
end
|
|
921
923
|
end
|
|
922
924
|
|
|
925
|
+
# === Explore Command ===
|
|
926
|
+
|
|
927
|
+
def run_explore(args)
|
|
928
|
+
options = { session_id: nil }
|
|
929
|
+
positional = []
|
|
930
|
+
skip_next = false
|
|
931
|
+
|
|
932
|
+
args.each_with_index do |arg, i|
|
|
933
|
+
if skip_next
|
|
934
|
+
skip_next = false
|
|
935
|
+
next
|
|
936
|
+
end
|
|
937
|
+
|
|
938
|
+
case arg
|
|
939
|
+
when "--session"
|
|
940
|
+
options[:session_id] = args[i + 1]
|
|
941
|
+
skip_next = true
|
|
942
|
+
when "--help", "-h"
|
|
943
|
+
print_explore_help
|
|
944
|
+
exit 0
|
|
945
|
+
else
|
|
946
|
+
positional << arg
|
|
947
|
+
end
|
|
948
|
+
end
|
|
949
|
+
|
|
950
|
+
explorer_html = File.expand_path("../tools/thread-explorer.html", __dir__)
|
|
951
|
+
|
|
952
|
+
# No args: just open the empty visualizer
|
|
953
|
+
unless positional[0]
|
|
954
|
+
open_in_browser("file://#{explorer_html}")
|
|
955
|
+
puts "Opened Thread Explorer in browser"
|
|
956
|
+
exit 0
|
|
957
|
+
end
|
|
958
|
+
|
|
959
|
+
env_path = resolve_environment(positional[0])
|
|
960
|
+
unless env_path
|
|
961
|
+
$stderr.puts "Error: environment '#{positional[0]}' not found"
|
|
962
|
+
exit 1
|
|
963
|
+
end
|
|
964
|
+
|
|
965
|
+
db_path = File.join(env_path, "sessions.db")
|
|
966
|
+
unless File.exist?(db_path)
|
|
967
|
+
$stderr.puts "No session data found (sessions.db not found)"
|
|
968
|
+
$stderr.puts "Opening empty Thread Explorer..."
|
|
969
|
+
open_in_browser("file://#{explorer_html}")
|
|
970
|
+
exit 0
|
|
971
|
+
end
|
|
972
|
+
|
|
973
|
+
store = PromptObjects::Session::Store.new(db_path)
|
|
974
|
+
|
|
975
|
+
if options[:session_id]
|
|
976
|
+
# Export a specific thread
|
|
977
|
+
export_and_open(store, options[:session_id], explorer_html)
|
|
978
|
+
else
|
|
979
|
+
# List sessions and let user choose, or export the most recent root thread
|
|
980
|
+
# Query root sessions directly (list_sessions requires po_name)
|
|
981
|
+
rows = store.instance_variable_get(:@db).execute(<<~SQL)
|
|
982
|
+
SELECT * FROM sessions
|
|
983
|
+
WHERE thread_type IS NULL OR thread_type = 'root'
|
|
984
|
+
ORDER BY created_at ASC
|
|
985
|
+
SQL
|
|
986
|
+
root_sessions = rows.map { |r| { id: r["id"], po_name: r["po_name"], name: r["name"], thread_type: r["thread_type"], created_at: r["created_at"] } }
|
|
987
|
+
|
|
988
|
+
if root_sessions.empty?
|
|
989
|
+
$stderr.puts "No root sessions found"
|
|
990
|
+
open_in_browser("file://#{explorer_html}")
|
|
991
|
+
exit 0
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
# Show available sessions
|
|
995
|
+
puts "Root threads:"
|
|
996
|
+
root_sessions.last(10).each_with_index do |s, i|
|
|
997
|
+
name = s[:name] || "(unnamed)"
|
|
998
|
+
po = s[:po_name] || "?"
|
|
999
|
+
time = s[:created_at]
|
|
1000
|
+
time_str = time.is_a?(Time) ? time.strftime("%Y-%m-%d %H:%M") : time.to_s[0, 16]
|
|
1001
|
+
puts " #{i + 1}. [#{po}] #{name} (#{time_str}) #{s[:id]}"
|
|
1002
|
+
end
|
|
1003
|
+
|
|
1004
|
+
puts
|
|
1005
|
+
puts "Opening most recent thread..."
|
|
1006
|
+
export_and_open(store, root_sessions.last[:id], explorer_html)
|
|
1007
|
+
end
|
|
1008
|
+
|
|
1009
|
+
store.close
|
|
1010
|
+
end
|
|
1011
|
+
|
|
1012
|
+
def export_and_open(store, session_id, explorer_html)
|
|
1013
|
+
data = store.export_thread_tree_json(session_id)
|
|
1014
|
+
unless data
|
|
1015
|
+
$stderr.puts "Error: session '#{session_id}' not found"
|
|
1016
|
+
exit 1
|
|
1017
|
+
end
|
|
1018
|
+
|
|
1019
|
+
json_str = JSON.generate(data)
|
|
1020
|
+
|
|
1021
|
+
# Read the explorer HTML and inject the data as auto-load
|
|
1022
|
+
html_content = File.read(explorer_html)
|
|
1023
|
+
inject_script = <<~JS
|
|
1024
|
+
<script>
|
|
1025
|
+
// Auto-load embedded data
|
|
1026
|
+
window.addEventListener('DOMContentLoaded', function() {
|
|
1027
|
+
var data = #{json_str};
|
|
1028
|
+
if (typeof loadData === 'function') loadData(data);
|
|
1029
|
+
});
|
|
1030
|
+
</script>
|
|
1031
|
+
JS
|
|
1032
|
+
|
|
1033
|
+
# Insert before </body>
|
|
1034
|
+
html_content = html_content.sub("</body>", "#{inject_script}</body>")
|
|
1035
|
+
|
|
1036
|
+
# Write to temp file
|
|
1037
|
+
require "tempfile"
|
|
1038
|
+
tmp = Tempfile.new(["thread-explorer-", ".html"])
|
|
1039
|
+
tmp.write(html_content)
|
|
1040
|
+
tmp.close
|
|
1041
|
+
|
|
1042
|
+
open_in_browser("file://#{tmp.path}")
|
|
1043
|
+
puts "Opened Thread Explorer for session #{session_id}"
|
|
1044
|
+
|
|
1045
|
+
# Keep temp file around briefly so the browser can load it
|
|
1046
|
+
sleep 2
|
|
1047
|
+
end
|
|
1048
|
+
|
|
1049
|
+
def open_in_browser(url)
|
|
1050
|
+
case RUBY_PLATFORM
|
|
1051
|
+
when /darwin/
|
|
1052
|
+
system("open", url)
|
|
1053
|
+
when /linux/
|
|
1054
|
+
system("xdg-open", url)
|
|
1055
|
+
when /mswin|mingw|cygwin/
|
|
1056
|
+
system("start", url)
|
|
1057
|
+
else
|
|
1058
|
+
$stderr.puts "Open this URL in your browser: #{url}"
|
|
1059
|
+
end
|
|
1060
|
+
end
|
|
1061
|
+
|
|
1062
|
+
def print_explore_help
|
|
1063
|
+
puts <<~HELP
|
|
1064
|
+
Usage: prompt_objects explore [env] [options]
|
|
1065
|
+
|
|
1066
|
+
Open the Thread Explorer to visualize conversation threads.
|
|
1067
|
+
|
|
1068
|
+
Arguments:
|
|
1069
|
+
env Environment name or path (optional)
|
|
1070
|
+
|
|
1071
|
+
Options:
|
|
1072
|
+
--session ID Open a specific thread by session ID
|
|
1073
|
+
--help, -h Show this help message
|
|
1074
|
+
|
|
1075
|
+
Examples:
|
|
1076
|
+
prompt_objects explore # Open empty explorer
|
|
1077
|
+
prompt_objects explore my-env # Open most recent thread
|
|
1078
|
+
prompt_objects explore my-env --session abc123 # Open specific thread
|
|
1079
|
+
HELP
|
|
1080
|
+
end
|
|
1081
|
+
|
|
923
1082
|
# === Main Entry Point ===
|
|
924
1083
|
|
|
925
1084
|
def run_env(args)
|
|
@@ -947,6 +1106,8 @@ def main
|
|
|
947
1106
|
run_message(args)
|
|
948
1107
|
when "events"
|
|
949
1108
|
run_events(args)
|
|
1109
|
+
when "explore"
|
|
1110
|
+
run_explore(args)
|
|
950
1111
|
when "help", "--help", "-h"
|
|
951
1112
|
print_main_help
|
|
952
1113
|
else
|
|
@@ -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 }
|
data/prompt_objects.gemspec
CHANGED
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Thread Explorer | PromptObjects</title>
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
|
|
10
|
+
<style>
|
|
11
|
+
/* ======== THEME ======== */
|
|
12
|
+
:root {
|
|
13
|
+
--bg-deep: #0a0e0a;
|
|
14
|
+
--bg: #0d120d;
|
|
15
|
+
--bg-raised: #131a13;
|
|
16
|
+
--bg-surface: #182018;
|
|
17
|
+
--border: #1e2b1e;
|
|
18
|
+
--border-bright: #2a3d2a;
|
|
19
|
+
--phosphor: #33ff66;
|
|
20
|
+
--phosphor-dim: #1a8033;
|
|
21
|
+
--phosphor-faint: #0d4020;
|
|
22
|
+
--phosphor-glow: rgba(51,255,102,0.15);
|
|
23
|
+
--amber: #ffaa33;
|
|
24
|
+
--amber-dim: #805515;
|
|
25
|
+
--amber-glow: rgba(255,170,51,0.12);
|
|
26
|
+
--cyan: #33ddff;
|
|
27
|
+
--cyan-dim: #1a7088;
|
|
28
|
+
--magenta: #dd55ff;
|
|
29
|
+
--magenta-dim: #6b2a80;
|
|
30
|
+
--red: #ff4455;
|
|
31
|
+
--red-dim: #661a22;
|
|
32
|
+
--pink: #ff6699;
|
|
33
|
+
--teal: #33ffcc;
|
|
34
|
+
--yellow: #ffdd44;
|
|
35
|
+
--text: #b8ccb8;
|
|
36
|
+
--text-dim: #667766;
|
|
37
|
+
--text-bright: #ddeedd;
|
|
38
|
+
--mono: 'IBM Plex Mono','Menlo',monospace;
|
|
39
|
+
}
|
|
40
|
+
* { margin:0; padding:0; box-sizing:border-box; }
|
|
41
|
+
html,body { height:100%; background:var(--bg-deep); color:var(--text); font-family:var(--mono); font-size:13px; line-height:1.6; overflow:hidden; }
|
|
42
|
+
body::after { content:''; position:fixed; inset:0; background:repeating-linear-gradient(0deg,transparent,transparent 2px,rgba(0,0,0,0.03) 2px,rgba(0,0,0,0.03) 4px); pointer-events:none; z-index:9999; }
|
|
43
|
+
::selection { background:var(--phosphor-dim); color:var(--text-bright); }
|
|
44
|
+
::-webkit-scrollbar { width:5px; height:5px; }
|
|
45
|
+
::-webkit-scrollbar-track { background:var(--bg); }
|
|
46
|
+
::-webkit-scrollbar-thumb { background:var(--border-bright); border-radius:3px; }
|
|
47
|
+
::-webkit-scrollbar-thumb:hover { background:var(--phosphor-dim); }
|
|
48
|
+
button { font-family:var(--mono); cursor:pointer; }
|
|
49
|
+
|
|
50
|
+
/* ======== DROP ZONE ======== */
|
|
51
|
+
#drop-zone { position:fixed; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:100; background:var(--bg-deep); transition:opacity 0.4s; }
|
|
52
|
+
#drop-zone.hidden { opacity:0; pointer-events:none; }
|
|
53
|
+
#drop-zone .logo { font-size:36px; font-weight:700; color:var(--phosphor); text-shadow:0 0 30px var(--phosphor-glow); letter-spacing:-1px; margin-bottom:6px; animation:flicker 4s ease-in-out infinite; }
|
|
54
|
+
#drop-zone .subtitle { font-size:11px; color:var(--text-dim); margin-bottom:40px; letter-spacing:3px; text-transform:uppercase; }
|
|
55
|
+
.drop-area { width:480px; height:240px; border:2px dashed var(--border-bright); border-radius:4px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; transition:all 0.2s; cursor:pointer; background:var(--bg); }
|
|
56
|
+
.drop-area:hover,.drop-area.dragover { border-color:var(--phosphor-dim); background:rgba(51,255,102,0.02); }
|
|
57
|
+
.drop-area .icon { font-size:40px; color:var(--phosphor-dim); transition:color 0.2s; }
|
|
58
|
+
.drop-area:hover .icon,.drop-area.dragover .icon { color:var(--phosphor); }
|
|
59
|
+
.drop-area p { color:var(--text-dim); font-size:13px; }
|
|
60
|
+
.drop-area p span { color:var(--phosphor); text-decoration:underline; text-underline-offset:3px; }
|
|
61
|
+
.drop-area .hint { font-size:11px; opacity:0.5; }
|
|
62
|
+
#file-input { display:none; }
|
|
63
|
+
.sample-link { margin-top:16px; font-size:11px; color:var(--text-dim); cursor:pointer; text-decoration:underline; text-underline-offset:3px; opacity:0.6; transition:opacity 0.2s; }
|
|
64
|
+
.sample-link:hover { opacity:1; color:var(--phosphor-dim); }
|
|
65
|
+
|
|
66
|
+
/* ======== APP LAYOUT ======== */
|
|
67
|
+
#app { display:none; height:100vh; grid-template-columns:1fr; grid-template-rows:auto auto 1fr; }
|
|
68
|
+
#app.visible { display:grid; }
|
|
69
|
+
|
|
70
|
+
/* Header */
|
|
71
|
+
#header { display:flex; align-items:center; gap:16px; padding:0 16px; height:40px; background:var(--bg); border-bottom:1px solid var(--border); }
|
|
72
|
+
.brand-name { font-weight:700; font-size:14px; color:var(--phosphor); text-shadow:0 0 20px var(--phosphor-glow); white-space:nowrap; }
|
|
73
|
+
.brand-sep { color:var(--border-bright); }
|
|
74
|
+
.thread-name { color:var(--text); font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
|
|
75
|
+
.header-stats { display:flex; gap:14px; font-size:11px; color:var(--text-dim); margin-left:auto; white-space:nowrap; }
|
|
76
|
+
.header-stats .v { color:var(--text); font-weight:500; }
|
|
77
|
+
.btn-open { background:none; border:1px solid var(--border-bright); color:var(--text-dim); font-size:10px; padding:3px 10px; border-radius:3px; transition:all 0.15s; margin-left:8px; }
|
|
78
|
+
.btn-open:hover { border-color:var(--phosphor-dim); color:var(--phosphor); }
|
|
79
|
+
|
|
80
|
+
/* Token cost bar */
|
|
81
|
+
#cost-bar { display:flex; height:20px; background:var(--bg); border-bottom:1px solid var(--border); padding:0 16px; align-items:center; gap:8px; }
|
|
82
|
+
#cost-bar .label { font-size:10px; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.5px; white-space:nowrap; }
|
|
83
|
+
#cost-bar .bar { flex:1; height:8px; background:var(--bg-deep); border-radius:2px; overflow:hidden; display:flex; }
|
|
84
|
+
#cost-bar .bar-seg { height:100%; transition:opacity 0.15s; cursor:pointer; position:relative; min-width:2px; }
|
|
85
|
+
#cost-bar .bar-seg:hover { opacity:0.8; }
|
|
86
|
+
#cost-bar .bar-legend { display:flex; gap:10px; font-size:10px; color:var(--text-dim); }
|
|
87
|
+
#cost-bar .bar-legend .dot { display:inline-block; width:6px; height:6px; border-radius:50%; margin-right:3px; vertical-align:middle; }
|
|
88
|
+
|
|
89
|
+
/* Toolbar */
|
|
90
|
+
#toolbar { display:flex; align-items:center; height:32px; background:var(--bg); border-bottom:1px solid var(--border); padding:0 16px; gap:8px; }
|
|
91
|
+
.tab { background:none; border:none; color:var(--text-dim); font-size:11px; padding:4px 12px; border-radius:3px; transition:all 0.15s; text-transform:uppercase; letter-spacing:0.5px; font-weight:500; }
|
|
92
|
+
.tab:hover { color:var(--text); background:var(--bg-raised); }
|
|
93
|
+
.tab.active { color:var(--phosphor); background:var(--phosphor-faint); }
|
|
94
|
+
.toolbar-sep { width:1px; height:16px; background:var(--border); }
|
|
95
|
+
#search { background:var(--bg-deep); border:1px solid var(--border); color:var(--text); font-family:var(--mono); font-size:11px; padding:3px 8px; border-radius:3px; width:200px; outline:none; transition:border-color 0.15s; }
|
|
96
|
+
#search:focus { border-color:var(--phosphor-dim); }
|
|
97
|
+
#search::placeholder { color:var(--text-dim); }
|
|
98
|
+
.filter-btns { display:flex; gap:4px; margin-left:auto; }
|
|
99
|
+
.filter-btn { background:none; border:1px solid var(--border); color:var(--text-dim); font-size:10px; padding:2px 8px; border-radius:3px; transition:all 0.15s; }
|
|
100
|
+
.filter-btn:hover { border-color:var(--border-bright); color:var(--text); }
|
|
101
|
+
.filter-btn.active { border-color:var(--phosphor-dim); color:var(--phosphor); background:var(--phosphor-faint); }
|
|
102
|
+
|
|
103
|
+
/* ======== MAIN CONTENT ======== */
|
|
104
|
+
#main { overflow:hidden; display:grid; }
|
|
105
|
+
|
|
106
|
+
/* Split view */
|
|
107
|
+
#main.split { grid-template-columns:1fr 1fr; }
|
|
108
|
+
#main.full { grid-template-columns:1fr; }
|
|
109
|
+
|
|
110
|
+
/* ======== SEQUENCE VIEW ======== */
|
|
111
|
+
#seq-view { overflow:auto; background:var(--bg-deep); }
|
|
112
|
+
.seq-actors { display:flex; position:sticky; top:0; z-index:5; background:var(--bg); border-bottom:1px solid var(--border); }
|
|
113
|
+
.seq-actor { flex:1; min-width:100px; text-align:center; padding:6px 4px; font-size:11px; font-weight:600; color:var(--text-dim); border-right:1px solid var(--border); cursor:pointer; transition:all 0.15s; position:relative; }
|
|
114
|
+
.seq-actor:last-child { border-right:none; }
|
|
115
|
+
.seq-actor:hover { background:var(--bg-raised); }
|
|
116
|
+
.seq-actor .actor-dot { display:inline-block; width:8px; height:8px; border-radius:50%; margin-right:4px; vertical-align:middle; }
|
|
117
|
+
.seq-actor.filtered { background:var(--phosphor-faint); color:var(--phosphor); }
|
|
118
|
+
.seq-events { position:relative; }
|
|
119
|
+
|
|
120
|
+
/* Lifelines */
|
|
121
|
+
.seq-lifelines { position:absolute; inset:0; display:flex; pointer-events:none; z-index:0; }
|
|
122
|
+
.seq-lifeline { flex:1; min-width:100px; border-right:1px dashed var(--border); position:relative; }
|
|
123
|
+
.seq-lifeline:last-child { border-right:none; }
|
|
124
|
+
|
|
125
|
+
/* Event rows */
|
|
126
|
+
.seq-row { position:relative; min-height:32px; display:flex; align-items:center; border-bottom:1px solid rgba(30,43,30,0.3); z-index:1; cursor:pointer; transition:background 0.1s; }
|
|
127
|
+
.seq-row:hover { background:rgba(51,255,102,0.02); }
|
|
128
|
+
.seq-row.active { background:rgba(51,255,102,0.05); }
|
|
129
|
+
.seq-row.structural { background:rgba(255,68,85,0.03); }
|
|
130
|
+
.seq-row .row-content { position:absolute; left:0; right:0; display:flex; align-items:center; padding:0; height:100%; }
|
|
131
|
+
|
|
132
|
+
/* Arrow rendering */
|
|
133
|
+
.seq-arrow-container { position:absolute; top:50%; height:0; display:flex; align-items:center; }
|
|
134
|
+
.seq-arrow-line { height:2px; flex:1; position:relative; }
|
|
135
|
+
.seq-arrow-head { width:0; height:0; border-top:5px solid transparent; border-bottom:5px solid transparent; flex-shrink:0; }
|
|
136
|
+
.seq-arrow-head.right { border-left:8px solid; }
|
|
137
|
+
.seq-arrow-head.left { border-right:8px solid; }
|
|
138
|
+
.seq-arrow-label { position:absolute; top:-16px; font-size:10px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%; padding:0 4px; }
|
|
139
|
+
.seq-arrow-label.above-right { left:4px; }
|
|
140
|
+
.seq-arrow-label.above-left { right:4px; text-align:right; }
|
|
141
|
+
|
|
142
|
+
/* Event type styling on arrows */
|
|
143
|
+
.seq-row.type-delegation .seq-arrow-line { height:3px; }
|
|
144
|
+
.seq-row.type-delegation_return .seq-arrow-line { height:1px; opacity:0.5; }
|
|
145
|
+
.seq-row.type-tool_call .seq-arrow-line { height:1px; opacity:0.6; }
|
|
146
|
+
.seq-row.type-tool_result .seq-arrow-line { height:1px; opacity:0.4; }
|
|
147
|
+
|
|
148
|
+
/* Structural event badge */
|
|
149
|
+
.seq-structural-badge { position:absolute; font-size:9px; padding:1px 6px; border-radius:2px; background:var(--red-dim); color:var(--red); font-weight:600; text-transform:uppercase; letter-spacing:0.5px; white-space:nowrap; top:50%; transform:translateY(-50%); }
|
|
150
|
+
|
|
151
|
+
/* Self-arrow (tool on same actor) */
|
|
152
|
+
.seq-self-arrow { position:absolute; width:30px; height:20px; border:1px solid; border-left:none; border-radius:0 6px 6px 0; top:50%; transform:translateY(-50%); }
|
|
153
|
+
|
|
154
|
+
/* Collapsed group */
|
|
155
|
+
.seq-group { position:relative; border-bottom:1px solid rgba(30,43,30,0.3); cursor:pointer; transition:background 0.1s; }
|
|
156
|
+
.seq-group:hover { background:rgba(51,255,102,0.02); }
|
|
157
|
+
.seq-group-header { display:flex; align-items:center; padding:4px 8px; min-height:28px; font-size:11px; color:var(--text-dim); gap:6px; }
|
|
158
|
+
.seq-group-header .chevron { font-size:9px; transition:transform 0.15s; width:10px; color:var(--text-dim); }
|
|
159
|
+
.seq-group-header.expanded .chevron { transform:rotate(90deg); }
|
|
160
|
+
.seq-group-header .group-label { opacity:0.7; }
|
|
161
|
+
.seq-group-body { display:none; }
|
|
162
|
+
.seq-group-body.expanded { display:block; }
|
|
163
|
+
|
|
164
|
+
/* ======== TIMELINE VIEW ======== */
|
|
165
|
+
#timeline-view { overflow:auto; background:var(--bg-deep); }
|
|
166
|
+
.tl-event { display:flex; align-items:flex-start; padding:6px 16px; gap:10px; border-bottom:1px solid rgba(30,43,30,0.3); cursor:pointer; transition:background 0.1s; min-height:32px; }
|
|
167
|
+
.tl-event:hover { background:rgba(51,255,102,0.02); }
|
|
168
|
+
.tl-event.active { background:rgba(51,255,102,0.05); }
|
|
169
|
+
.tl-event.structural { border-left:2px solid var(--red); }
|
|
170
|
+
.tl-idx { font-size:10px; color:var(--text-dim); width:28px; text-align:right; flex-shrink:0; padding-top:2px; opacity:0.5; }
|
|
171
|
+
.tl-time { font-size:10px; color:var(--text-dim); width:60px; flex-shrink:0; padding-top:2px; }
|
|
172
|
+
.tl-actor-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; margin-top:5px; }
|
|
173
|
+
.tl-body { flex:1; min-width:0; }
|
|
174
|
+
.tl-header-line { display:flex; align-items:center; gap:6px; font-size:12px; }
|
|
175
|
+
.tl-from { font-weight:600; }
|
|
176
|
+
.tl-arrow { color:var(--text-dim); font-size:10px; }
|
|
177
|
+
.tl-to { font-weight:500; }
|
|
178
|
+
.tl-type-badge { font-size:9px; padding:1px 5px; border-radius:2px; text-transform:uppercase; letter-spacing:0.3px; font-weight:600; margin-left:4px; }
|
|
179
|
+
.tl-type-badge.delegation { background:rgba(255,170,51,0.15); color:var(--amber); }
|
|
180
|
+
.tl-type-badge.structural { background:rgba(255,68,85,0.15); color:var(--red); }
|
|
181
|
+
.tl-type-badge.tool { background:rgba(102,119,102,0.15); color:var(--text-dim); }
|
|
182
|
+
.tl-preview { font-size:11px; color:var(--text-dim); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; margin-top:2px; max-width:600px; }
|
|
183
|
+
.tl-usage { font-size:10px; color:var(--text-dim); opacity:0.5; margin-top:1px; }
|
|
184
|
+
.tl-depth-indent { display:inline-block; }
|
|
185
|
+
|
|
186
|
+
/* ======== DETAIL VIEW ======== */
|
|
187
|
+
#detail-view { overflow:auto; background:var(--bg-deep); border-left:1px solid var(--border); }
|
|
188
|
+
.detail-empty { display:flex; flex-direction:column; align-items:center; justify-content:center; height:100%; color:var(--text-dim); gap:6px; }
|
|
189
|
+
.detail-empty .icon { font-size:28px; opacity:0.3; }
|
|
190
|
+
.detail-header { position:sticky; top:0; z-index:5; background:var(--bg); border-bottom:1px solid var(--border); padding:10px 16px; display:flex; align-items:center; justify-content:space-between; }
|
|
191
|
+
.detail-header .po-name { font-weight:700; font-size:15px; color:var(--text-bright); }
|
|
192
|
+
.detail-header .type-badge { font-size:9px; text-transform:uppercase; letter-spacing:0.5px; padding:2px 6px; border-radius:2px; font-weight:600; margin-left:8px; }
|
|
193
|
+
.badge-root { background:var(--phosphor-faint); color:var(--phosphor); }
|
|
194
|
+
.badge-delegation { background:rgba(255,170,51,0.1); color:var(--amber); }
|
|
195
|
+
.detail-header .parent-info { font-size:10px; color:var(--text-dim); margin-left:8px; }
|
|
196
|
+
.detail-header .parent-info em { color:var(--amber); font-style:normal; }
|
|
197
|
+
.detail-header .stats { font-size:10px; color:var(--text-dim); display:flex; gap:12px; }
|
|
198
|
+
.detail-header .stats .v { color:var(--text); }
|
|
199
|
+
|
|
200
|
+
/* Breadcrumbs */
|
|
201
|
+
.breadcrumbs { padding:6px 16px; background:var(--bg); border-bottom:1px solid var(--border); font-size:11px; display:flex; align-items:center; gap:4px; }
|
|
202
|
+
.breadcrumb { color:var(--text-dim); cursor:pointer; transition:color 0.15s; }
|
|
203
|
+
.breadcrumb:hover { color:var(--phosphor); }
|
|
204
|
+
.breadcrumb.current { color:var(--text-bright); cursor:default; }
|
|
205
|
+
.breadcrumb-sep { color:var(--border-bright); }
|
|
206
|
+
|
|
207
|
+
/* Messages */
|
|
208
|
+
.messages { padding:12px 16px 60px; }
|
|
209
|
+
.msg { margin-bottom:2px; animation:fadeIn 0.12s ease; }
|
|
210
|
+
.msg-head { display:flex; align-items:center; gap:6px; padding-top:10px; margin-bottom:4px; }
|
|
211
|
+
.msg-sender { font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:0.3px; }
|
|
212
|
+
.msg-sender.human { color:var(--cyan); }
|
|
213
|
+
.msg-sender.assistant { color:var(--phosphor); }
|
|
214
|
+
.msg-sender.delegator { color:var(--amber); }
|
|
215
|
+
.msg-ts { font-size:10px; color:var(--text-dim); opacity:0.4; }
|
|
216
|
+
.msg-tokens { font-size:10px; color:var(--text-dim); margin-left:auto; }
|
|
217
|
+
.msg-body { padding:8px 12px; border-radius:3px; font-size:12px; line-height:1.65; white-space:pre-wrap; word-break:break-word; }
|
|
218
|
+
.msg.user .msg-body { background:rgba(51,221,255,0.04); border-left:2px solid var(--cyan-dim); }
|
|
219
|
+
.msg.user.from-po .msg-body { background:rgba(255,170,51,0.04); border-left:2px solid var(--amber-dim); }
|
|
220
|
+
.msg.assistant .msg-body { background:rgba(51,255,102,0.03); border-left:2px solid var(--phosphor-faint); }
|
|
221
|
+
.msg.highlighted .msg-body { box-shadow:inset 0 0 0 1px var(--phosphor-dim); }
|
|
222
|
+
|
|
223
|
+
/* Tool calls in detail */
|
|
224
|
+
.tc { margin:3px 0; border:1px solid var(--border); border-radius:3px; overflow:hidden; }
|
|
225
|
+
.tc-head { display:flex; align-items:center; gap:6px; padding:6px 10px; background:var(--bg-raised); cursor:pointer; font-size:11px; user-select:none; transition:background 0.1s; }
|
|
226
|
+
.tc-head:hover { background:var(--bg-surface); }
|
|
227
|
+
.tc-head .chv { color:var(--text-dim); font-size:9px; width:10px; transition:transform 0.15s; }
|
|
228
|
+
.tc-head.open .chv { transform:rotate(90deg); }
|
|
229
|
+
.tc-head .tc-name { font-weight:600; color:var(--phosphor-dim); }
|
|
230
|
+
.tc-head .tc-label { color:var(--text-dim); font-size:10px; }
|
|
231
|
+
.tc-body { display:none; border-top:1px solid var(--border); }
|
|
232
|
+
.tc-body.open { display:block; }
|
|
233
|
+
.tc-section { padding:8px 10px; }
|
|
234
|
+
.tc-section + .tc-section { border-top:1px solid var(--border); }
|
|
235
|
+
.tc-section-label { font-size:9px; text-transform:uppercase; letter-spacing:0.5px; color:var(--text-dim); margin-bottom:4px; }
|
|
236
|
+
.tc-section pre { font-family:var(--mono); font-size:11px; line-height:1.5; color:var(--text); white-space:pre-wrap; word-break:break-all; max-height:250px; overflow-y:auto; }
|
|
237
|
+
.tc-result { max-height:150px; overflow-y:auto; font-size:11px; color:var(--text-dim); white-space:pre-wrap; word-break:break-all; line-height:1.5; }
|
|
238
|
+
|
|
239
|
+
/* Delegation marker in detail */
|
|
240
|
+
.deleg-marker { display:flex; align-items:center; gap:6px; padding:6px 10px; margin:4px 0; border:1px dashed var(--amber-dim); border-radius:3px; background:rgba(255,170,51,0.03); font-size:11px; color:var(--amber); cursor:pointer; transition:all 0.15s; }
|
|
241
|
+
.deleg-marker:hover { background:rgba(255,170,51,0.06); border-color:var(--amber); }
|
|
242
|
+
.deleg-marker .dm-info { opacity:0.5; margin-left:auto; font-size:10px; }
|
|
243
|
+
|
|
244
|
+
/* ======== ANIMATIONS ======== */
|
|
245
|
+
@keyframes flicker { 0%,100%{opacity:1} 92%{opacity:1} 93%{opacity:0.8} 94%{opacity:1} 96%{opacity:0.9} 97%{opacity:1} }
|
|
246
|
+
@keyframes fadeIn { from{opacity:0;transform:translateY(3px)} to{opacity:1;transform:translateY(0)} }
|
|
247
|
+
</style>
|
|
248
|
+
</head>
|
|
249
|
+
<body>
|
|
250
|
+
|
|
251
|
+
<div id="drop-zone">
|
|
252
|
+
<div class="logo">Thread Explorer</div>
|
|
253
|
+
<div class="subtitle">PromptObjects</div>
|
|
254
|
+
<div class="drop-area" id="drop-area">
|
|
255
|
+
<div class="icon">{...}</div>
|
|
256
|
+
<p>Drop a thread export <span>JSON</span> here</p>
|
|
257
|
+
<p class="hint">or click to browse</p>
|
|
258
|
+
</div>
|
|
259
|
+
<input type="file" id="file-input" accept=".json,application/json">
|
|
260
|
+
<div class="sample-link" id="load-sample">load sample data</div>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div id="app">
|
|
264
|
+
<div id="header">
|
|
265
|
+
<span class="brand-name">Thread Explorer</span>
|
|
266
|
+
<span class="brand-sep">/</span>
|
|
267
|
+
<span class="thread-name" id="h-name">---</span>
|
|
268
|
+
<div class="header-stats" id="h-stats"></div>
|
|
269
|
+
<button class="btn-open" id="btn-open">Open</button>
|
|
270
|
+
</div>
|
|
271
|
+
<div id="cost-bar">
|
|
272
|
+
<span class="label">Tokens</span>
|
|
273
|
+
<div class="bar" id="cost-bar-inner"></div>
|
|
274
|
+
<div class="bar-legend" id="cost-legend"></div>
|
|
275
|
+
</div>
|
|
276
|
+
<div id="main" class="split">
|
|
277
|
+
<div style="display:flex;flex-direction:column;overflow:hidden;">
|
|
278
|
+
<div id="toolbar">
|
|
279
|
+
<button class="tab active" data-view="sequence">Sequence</button>
|
|
280
|
+
<button class="tab" data-view="timeline">Timeline</button>
|
|
281
|
+
<div class="toolbar-sep"></div>
|
|
282
|
+
<input id="search" type="text" placeholder="Search messages...">
|
|
283
|
+
<div class="filter-btns" id="filter-btns"></div>
|
|
284
|
+
</div>
|
|
285
|
+
<div id="seq-view" style="flex:1;overflow:auto;"></div>
|
|
286
|
+
<div id="timeline-view" style="flex:1;overflow:auto;display:none;"></div>
|
|
287
|
+
</div>
|
|
288
|
+
<div id="detail-view">
|
|
289
|
+
<div class="detail-empty"><div class="icon">←</div><div>Select an event</div></div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<script>
|
|
295
|
+
// ========================================
|
|
296
|
+
// THREAD EXPLORER
|
|
297
|
+
// ========================================
|
|
298
|
+
|
|
299
|
+
// --- State ---
|
|
300
|
+
let treeData = null;
|
|
301
|
+
let events = []; // flattened event sequence
|
|
302
|
+
let actors = []; // ordered actor names
|
|
303
|
+
let poNames = new Set();
|
|
304
|
+
let actorColors = {};
|
|
305
|
+
let activeView = 'sequence';
|
|
306
|
+
let selectedEventIdx = null;
|
|
307
|
+
let selectedSessionId = null;
|
|
308
|
+
let searchQuery = '';
|
|
309
|
+
let activeFilters = new Set(); // PO name filters
|
|
310
|
+
let allNodes = {}; // id -> node for quick lookup
|
|
311
|
+
|
|
312
|
+
const COLORS = ['#33ff66','#ffaa33','#33ddff','#dd55ff','#ff6699','#33ffcc','#ffdd44','#88aaff','#ff8844','#99ff33'];
|
|
313
|
+
const STRUCTURAL_TOOLS = new Set(['create_capability','add_capability','request_capability','remove_capability','modify_prompt','create_po','delete_primitive','ask_human']);
|
|
314
|
+
|
|
315
|
+
// --- DOM refs ---
|
|
316
|
+
const dropZone = document.getElementById('drop-zone');
|
|
317
|
+
const dropArea = document.getElementById('drop-area');
|
|
318
|
+
const fileInput = document.getElementById('file-input');
|
|
319
|
+
const app = document.getElementById('app');
|
|
320
|
+
|
|
321
|
+
// --- File loading ---
|
|
322
|
+
dropArea.addEventListener('click', () => fileInput.click());
|
|
323
|
+
fileInput.addEventListener('change', e => { if(e.target.files[0]) loadFile(e.target.files[0]); });
|
|
324
|
+
dropArea.addEventListener('dragover', e => { e.preventDefault(); dropArea.classList.add('dragover'); });
|
|
325
|
+
dropArea.addEventListener('dragleave', () => dropArea.classList.remove('dragover'));
|
|
326
|
+
dropArea.addEventListener('drop', e => { e.preventDefault(); dropArea.classList.remove('dragover'); if(e.dataTransfer.files[0]) loadFile(e.dataTransfer.files[0]); });
|
|
327
|
+
document.getElementById('btn-open').addEventListener('click', () => { const i=document.createElement('input'); i.type='file'; i.accept='.json'; i.onchange=e=>{if(e.target.files[0])loadFile(e.target.files[0]);}; i.click(); });
|
|
328
|
+
document.getElementById('load-sample').addEventListener('click', () => loadData(generateSampleData()));
|
|
329
|
+
|
|
330
|
+
function loadFile(file) {
|
|
331
|
+
const r = new FileReader();
|
|
332
|
+
r.onload = e => { try { loadData(JSON.parse(e.target.result)); } catch(err) { alert('Parse error: '+err.message); } };
|
|
333
|
+
r.readAsText(file);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function loadData(data) {
|
|
337
|
+
treeData = data;
|
|
338
|
+
allNodes = {};
|
|
339
|
+
indexNodes(data);
|
|
340
|
+
poNames = extractPONames(data);
|
|
341
|
+
events = extractEvents(data);
|
|
342
|
+
actors = extractActors();
|
|
343
|
+
assignColors();
|
|
344
|
+
selectedEventIdx = null;
|
|
345
|
+
selectedSessionId = null;
|
|
346
|
+
activeFilters = new Set();
|
|
347
|
+
searchQuery = '';
|
|
348
|
+
document.getElementById('search').value = '';
|
|
349
|
+
|
|
350
|
+
dropZone.classList.add('hidden');
|
|
351
|
+
app.classList.add('visible');
|
|
352
|
+
|
|
353
|
+
renderAll();
|
|
354
|
+
// Auto-select first event
|
|
355
|
+
if (events.length > 0) selectEvent(0);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function indexNodes(node, parent) {
|
|
359
|
+
const id = node.session?.id || 'n-'+Object.keys(allNodes).length;
|
|
360
|
+
allNodes[id] = { ...node, _parent: parent || null };
|
|
361
|
+
(node.children||[]).forEach(c => indexNodes(c, node));
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function extractPONames(node) {
|
|
365
|
+
const s = new Set();
|
|
366
|
+
s.add(node.session?.po_name);
|
|
367
|
+
(node.children||[]).forEach(c => extractPONames(c).forEach(n => s.add(n)));
|
|
368
|
+
return s;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function extractActors() {
|
|
372
|
+
const ordered = ['human'];
|
|
373
|
+
const seen = new Set(['human']);
|
|
374
|
+
events.forEach(e => {
|
|
375
|
+
[e.from, e.to].forEach(a => {
|
|
376
|
+
if (a && !seen.has(a) && (poNames.has(a) || a === 'human')) { seen.add(a); ordered.push(a); }
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
// Add "tools" pseudo-actor if any tool calls exist
|
|
380
|
+
if (events.some(e => e.type === 'tool_call' || e.type === 'tool_result')) {
|
|
381
|
+
ordered.push('[tools]');
|
|
382
|
+
}
|
|
383
|
+
return ordered;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function assignColors() {
|
|
387
|
+
actorColors = {};
|
|
388
|
+
let ci = 0;
|
|
389
|
+
actors.forEach(a => {
|
|
390
|
+
if (a === 'human') actorColors[a] = '#33ddff';
|
|
391
|
+
else if (a === '[tools]') actorColors[a] = '#667766';
|
|
392
|
+
else { actorColors[a] = COLORS[ci % COLORS.length]; ci++; }
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// --- Event extraction ---
|
|
397
|
+
function extractEvents(node, depth) {
|
|
398
|
+
depth = depth || 0;
|
|
399
|
+
const evts = [];
|
|
400
|
+
const session = node.session || {};
|
|
401
|
+
const poName = session.po_name || 'unknown';
|
|
402
|
+
const messages = node.messages || [];
|
|
403
|
+
|
|
404
|
+
// Delegation children lookup
|
|
405
|
+
const delegKids = {};
|
|
406
|
+
(node.children||[]).forEach(c => {
|
|
407
|
+
const cp = c.session?.po_name;
|
|
408
|
+
if (c.session?.thread_type === 'delegation' && cp) {
|
|
409
|
+
if (!delegKids[cp]) delegKids[cp] = [];
|
|
410
|
+
delegKids[cp].push(c);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
const delegCallIds = new Set();
|
|
414
|
+
|
|
415
|
+
for (const msg of messages) {
|
|
416
|
+
if (msg.role === 'user') {
|
|
417
|
+
evts.push({
|
|
418
|
+
type: 'message', from: msg.from_po || 'human', to: poName,
|
|
419
|
+
content: msg.content || '', summary: trunc(msg.content||'',80),
|
|
420
|
+
sessionId: session.id, depth, ts: msg.created_at, usage: null, isStructural: false
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
if (msg.role === 'assistant') {
|
|
424
|
+
if (msg.tool_calls) {
|
|
425
|
+
for (const tc of msg.tool_calls) {
|
|
426
|
+
const tcName = tc.name||'';
|
|
427
|
+
const tcId = tc.id||'';
|
|
428
|
+
const isDeleg = delegKids[tcName] && delegKids[tcName].length > 0;
|
|
429
|
+
if (isDeleg) {
|
|
430
|
+
delegCallIds.add(tcId);
|
|
431
|
+
evts.push({
|
|
432
|
+
type: 'delegation', from: poName, to: tcName,
|
|
433
|
+
content: JSON.stringify(tc.arguments||{}), summary: 'delegate \u2192 '+tcName,
|
|
434
|
+
sessionId: session.id, depth, ts: msg.created_at, usage: msg.usage, isStructural: false
|
|
435
|
+
});
|
|
436
|
+
const child = delegKids[tcName].shift();
|
|
437
|
+
evts.push(...extractEvents(child, depth+1));
|
|
438
|
+
evts.push({
|
|
439
|
+
type: 'delegation_return', from: tcName, to: poName,
|
|
440
|
+
content: '', summary: tcName+' \u2192 return',
|
|
441
|
+
sessionId: session.id, depth, ts: msg.created_at, usage: null, isStructural: false
|
|
442
|
+
});
|
|
443
|
+
} else {
|
|
444
|
+
const isSt = STRUCTURAL_TOOLS.has(tcName);
|
|
445
|
+
evts.push({
|
|
446
|
+
type: isSt ? 'structural' : 'tool_call', from: poName, to: isSt ? poName : '[tools]',
|
|
447
|
+
content: JSON.stringify(tc.arguments||{}), summary: tcName, toolName: tcName,
|
|
448
|
+
sessionId: session.id, depth, ts: msg.created_at, usage: msg.usage, isStructural: isSt,
|
|
449
|
+
structuralType: isSt ? tcName : null, toolCallId: tcId
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (msg.content) {
|
|
455
|
+
evts.push({
|
|
456
|
+
type: 'response', from: poName, to: session.parent_po || 'human',
|
|
457
|
+
content: msg.content, summary: trunc(msg.content,80),
|
|
458
|
+
sessionId: session.id, depth, ts: msg.created_at, usage: msg.usage, isStructural: false
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (msg.role === 'tool') {
|
|
463
|
+
for (const tr of (msg.tool_results||[])) {
|
|
464
|
+
if (delegCallIds.has(tr.tool_call_id)) continue;
|
|
465
|
+
evts.push({
|
|
466
|
+
type: 'tool_result', from: '[tools]', to: poName,
|
|
467
|
+
content: tr.content||'', summary: trunc(tr.content||'',60), toolName: tr.name,
|
|
468
|
+
sessionId: session.id, depth, ts: msg.created_at, usage: null, isStructural: false,
|
|
469
|
+
toolCallId: tr.tool_call_id
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return evts;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// --- Rendering orchestration ---
|
|
478
|
+
function renderAll() {
|
|
479
|
+
renderHeader();
|
|
480
|
+
renderCostBar();
|
|
481
|
+
renderFilterBtns();
|
|
482
|
+
renderSequenceView();
|
|
483
|
+
renderTimelineView();
|
|
484
|
+
renderDetail();
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function getFilteredEvents() {
|
|
488
|
+
let filtered = events;
|
|
489
|
+
if (activeFilters.size > 0) {
|
|
490
|
+
filtered = filtered.filter(e => activeFilters.has(e.from) || activeFilters.has(e.to));
|
|
491
|
+
}
|
|
492
|
+
if (searchQuery) {
|
|
493
|
+
const q = searchQuery.toLowerCase();
|
|
494
|
+
filtered = filtered.filter(e => (e.content||'').toLowerCase().includes(q) || (e.summary||'').toLowerCase().includes(q) || (e.toolName||'').toLowerCase().includes(q));
|
|
495
|
+
}
|
|
496
|
+
return filtered;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// --- Header ---
|
|
500
|
+
function renderHeader() {
|
|
501
|
+
const s = treeData.session||{};
|
|
502
|
+
document.getElementById('h-name').textContent = s.name || s.po_name || 'Untitled';
|
|
503
|
+
const usage = sumUsageTree(treeData);
|
|
504
|
+
const nEvents = events.length;
|
|
505
|
+
const nSessions = Object.keys(allNodes).length;
|
|
506
|
+
document.getElementById('h-stats').innerHTML =
|
|
507
|
+
`<span><span class="v">${nSessions}</span> sessions</span>`+
|
|
508
|
+
`<span><span class="v">${nEvents}</span> events</span>`+
|
|
509
|
+
`<span><span class="v">${fmtNum(usage.input)}</span> in</span>`+
|
|
510
|
+
`<span><span class="v">${fmtNum(usage.output)}</span> out</span>`;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// --- Cost bar ---
|
|
514
|
+
function renderCostBar() {
|
|
515
|
+
const perPo = {};
|
|
516
|
+
events.forEach(e => {
|
|
517
|
+
if (e.usage) {
|
|
518
|
+
const po = e.from;
|
|
519
|
+
if (!perPo[po]) perPo[po] = { input:0, output:0 };
|
|
520
|
+
perPo[po].input += e.usage.input_tokens||0;
|
|
521
|
+
perPo[po].output += e.usage.output_tokens||0;
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
const total = Object.values(perPo).reduce((s,v) => s+v.input+v.output, 0) || 1;
|
|
525
|
+
const bar = document.getElementById('cost-bar-inner');
|
|
526
|
+
const legend = document.getElementById('cost-legend');
|
|
527
|
+
bar.innerHTML = '';
|
|
528
|
+
legend.innerHTML = '';
|
|
529
|
+
Object.entries(perPo).forEach(([po, v]) => {
|
|
530
|
+
const pct = ((v.input+v.output)/total*100);
|
|
531
|
+
if (pct < 0.5) return;
|
|
532
|
+
const seg = document.createElement('div');
|
|
533
|
+
seg.className = 'bar-seg';
|
|
534
|
+
seg.style.width = pct+'%';
|
|
535
|
+
seg.style.background = actorColors[po] || '#667766';
|
|
536
|
+
seg.title = `${po}: ${fmtNum(v.input)} in, ${fmtNum(v.output)} out`;
|
|
537
|
+
seg.addEventListener('click', () => toggleFilter(po));
|
|
538
|
+
bar.appendChild(seg);
|
|
539
|
+
legend.innerHTML += `<span><span class="dot" style="background:${actorColors[po]||'#667'}"></span>${esc(po)}</span>`;
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// --- Filter buttons ---
|
|
544
|
+
function renderFilterBtns() {
|
|
545
|
+
const container = document.getElementById('filter-btns');
|
|
546
|
+
container.innerHTML = '';
|
|
547
|
+
actors.filter(a => a !== '[tools]').forEach(a => {
|
|
548
|
+
const btn = document.createElement('button');
|
|
549
|
+
btn.className = 'filter-btn' + (activeFilters.has(a) ? ' active' : '');
|
|
550
|
+
btn.textContent = a;
|
|
551
|
+
btn.style.borderColor = activeFilters.has(a) ? (actorColors[a]||'') : '';
|
|
552
|
+
btn.style.color = activeFilters.has(a) ? (actorColors[a]||'') : '';
|
|
553
|
+
btn.addEventListener('click', () => toggleFilter(a));
|
|
554
|
+
container.appendChild(btn);
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function toggleFilter(actor) {
|
|
559
|
+
if (activeFilters.has(actor)) activeFilters.delete(actor);
|
|
560
|
+
else activeFilters.add(actor);
|
|
561
|
+
renderFilterBtns();
|
|
562
|
+
renderSequenceView();
|
|
563
|
+
renderTimelineView();
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// --- Search ---
|
|
567
|
+
document.getElementById('search').addEventListener('input', e => {
|
|
568
|
+
searchQuery = e.target.value;
|
|
569
|
+
renderSequenceView();
|
|
570
|
+
renderTimelineView();
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
// --- Tab switching ---
|
|
574
|
+
document.querySelectorAll('.tab').forEach(tab => {
|
|
575
|
+
tab.addEventListener('click', () => {
|
|
576
|
+
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
577
|
+
tab.classList.add('active');
|
|
578
|
+
activeView = tab.dataset.view;
|
|
579
|
+
document.getElementById('seq-view').style.display = activeView === 'sequence' ? 'block' : 'none';
|
|
580
|
+
document.getElementById('timeline-view').style.display = activeView === 'timeline' ? 'block' : 'none';
|
|
581
|
+
});
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// ========================================
|
|
585
|
+
// SEQUENCE VIEW
|
|
586
|
+
// ========================================
|
|
587
|
+
function renderSequenceView() {
|
|
588
|
+
const container = document.getElementById('seq-view');
|
|
589
|
+
const filtered = getFilteredEvents();
|
|
590
|
+
|
|
591
|
+
// Actor header
|
|
592
|
+
let html = '<div class="seq-actors">';
|
|
593
|
+
actors.forEach(a => {
|
|
594
|
+
const col = actorColors[a]||'#667';
|
|
595
|
+
const cls = activeFilters.has(a) ? ' filtered' : '';
|
|
596
|
+
html += `<div class="seq-actor${cls}" onclick="toggleFilter('${esc(a)}')" style="color:${col}"><span class="actor-dot" style="background:${col}"></span>${esc(a)}</div>`;
|
|
597
|
+
});
|
|
598
|
+
html += '</div>';
|
|
599
|
+
|
|
600
|
+
// Events area
|
|
601
|
+
html += '<div class="seq-events">';
|
|
602
|
+
// Lifelines
|
|
603
|
+
html += '<div class="seq-lifelines">';
|
|
604
|
+
actors.forEach(() => { html += '<div class="seq-lifeline"></div>'; });
|
|
605
|
+
html += '</div>';
|
|
606
|
+
|
|
607
|
+
// Group consecutive tool_call/tool_result pairs by same PO
|
|
608
|
+
const groups = groupToolCalls(filtered);
|
|
609
|
+
|
|
610
|
+
groups.forEach(g => {
|
|
611
|
+
if (g.group) {
|
|
612
|
+
html += renderSeqGroup(g);
|
|
613
|
+
} else {
|
|
614
|
+
html += renderSeqRow(g.event, g.originalIdx);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
html += '</div>';
|
|
619
|
+
container.innerHTML = html;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function groupToolCalls(evts) {
|
|
623
|
+
const result = [];
|
|
624
|
+
let i = 0;
|
|
625
|
+
while (i < evts.length) {
|
|
626
|
+
const e = evts[i];
|
|
627
|
+
if ((e.type === 'tool_call' || e.type === 'tool_result') && !e.isStructural) {
|
|
628
|
+
// Collect consecutive tool events from the same session
|
|
629
|
+
const groupEvents = [];
|
|
630
|
+
const sessionId = e.sessionId;
|
|
631
|
+
while (i < evts.length && (evts[i].type === 'tool_call' || evts[i].type === 'tool_result') && evts[i].sessionId === sessionId && !evts[i].isStructural) {
|
|
632
|
+
groupEvents.push({ event: evts[i], originalIdx: events.indexOf(evts[i]) });
|
|
633
|
+
i++;
|
|
634
|
+
}
|
|
635
|
+
if (groupEvents.length > 2) {
|
|
636
|
+
result.push({ group: true, events: groupEvents, sessionId });
|
|
637
|
+
} else {
|
|
638
|
+
groupEvents.forEach(ge => result.push(ge));
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
result.push({ event: e, originalIdx: events.indexOf(e) });
|
|
642
|
+
i++;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function renderSeqGroup(g) {
|
|
649
|
+
const toolNames = [...new Set(g.events.filter(e=>e.event.type==='tool_call').map(e=>e.event.toolName||e.event.summary))];
|
|
650
|
+
const count = g.events.filter(e=>e.event.type==='tool_call').length;
|
|
651
|
+
const firstEvt = g.events[0].event;
|
|
652
|
+
const fromIdx = actors.indexOf(firstEvt.from);
|
|
653
|
+
const col = actorColors[firstEvt.from]||'#667';
|
|
654
|
+
|
|
655
|
+
let html = `<div class="seq-group">`;
|
|
656
|
+
html += `<div class="seq-group-header" onclick="toggleSeqGroup(this)">`;
|
|
657
|
+
html += `<span class="chevron">▶</span>`;
|
|
658
|
+
html += `<span style="color:${col}">${esc(firstEvt.from)}</span>`;
|
|
659
|
+
html += `<span class="group-label">${count} tool calls: ${esc(toolNames.slice(0,3).join(', '))}${toolNames.length>3?'...':''}</span>`;
|
|
660
|
+
html += `</div>`;
|
|
661
|
+
html += `<div class="seq-group-body">`;
|
|
662
|
+
g.events.forEach(ge => { html += renderSeqRow(ge.event, ge.originalIdx); });
|
|
663
|
+
html += `</div></div>`;
|
|
664
|
+
return html;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function renderSeqRow(evt, idx) {
|
|
668
|
+
const fromIdx = actors.indexOf(evt.from);
|
|
669
|
+
const toIdx = actors.indexOf(evt.to);
|
|
670
|
+
const isActive = idx === selectedEventIdx;
|
|
671
|
+
|
|
672
|
+
let cls = `seq-row type-${evt.type}${isActive?' active':''}${evt.isStructural?' structural':''}`;
|
|
673
|
+
let html = `<div class="${cls}" data-idx="${idx}" onclick="selectEvent(${idx})">`;
|
|
674
|
+
html += `<div class="row-content">`;
|
|
675
|
+
|
|
676
|
+
if (evt.isStructural) {
|
|
677
|
+
// Structural badge on actor's lifeline
|
|
678
|
+
const x = fromIdx >= 0 ? ((fromIdx + 0.5) / actors.length * 100) : 50;
|
|
679
|
+
const col = actorColors[evt.from]||'#667';
|
|
680
|
+
html += `<div class="seq-structural-badge" style="left:calc(${x}% + 12px);background:rgba(255,68,85,0.15);color:var(--red);">${esc(evt.structuralType||evt.summary)}</div>`;
|
|
681
|
+
} else if (fromIdx >= 0 && toIdx >= 0 && fromIdx !== toIdx) {
|
|
682
|
+
// Arrow between two different actors
|
|
683
|
+
const left = Math.min(fromIdx, toIdx);
|
|
684
|
+
const right = Math.max(fromIdx, toIdx);
|
|
685
|
+
const goesRight = toIdx > fromIdx;
|
|
686
|
+
const leftPct = ((left + 0.5) / actors.length * 100);
|
|
687
|
+
const rightPct = ((right + 0.5) / actors.length * 100);
|
|
688
|
+
const col = actorColors[evt.from]||'#667';
|
|
689
|
+
const labelText = evt.type === 'tool_result' ? (evt.toolName||'\u2190') : evt.summary;
|
|
690
|
+
|
|
691
|
+
html += `<div class="seq-arrow-container" style="left:${leftPct}%;width:${rightPct-leftPct}%;">`;
|
|
692
|
+
if (!goesRight) html += `<div class="seq-arrow-head left" style="border-right-color:${col}"></div>`;
|
|
693
|
+
html += `<div class="seq-arrow-line" style="background:${col}">`;
|
|
694
|
+
html += `<div class="seq-arrow-label ${goesRight?'above-right':'above-left'}" style="color:${col}">${esc(trunc(labelText,40))}</div>`;
|
|
695
|
+
html += `</div>`;
|
|
696
|
+
if (goesRight) html += `<div class="seq-arrow-head right" style="border-left-color:${col}"></div>`;
|
|
697
|
+
html += `</div>`;
|
|
698
|
+
} else if (fromIdx >= 0 && fromIdx === toIdx) {
|
|
699
|
+
// Self-arrow (rare)
|
|
700
|
+
const x = ((fromIdx + 0.5) / actors.length * 100);
|
|
701
|
+
const col = actorColors[evt.from]||'#667';
|
|
702
|
+
html += `<div class="seq-self-arrow" style="left:calc(${x}% + 4px);border-color:${col}"></div>`;
|
|
703
|
+
html += `<div class="seq-structural-badge" style="left:calc(${x}% + 40px);background:transparent;color:${col}">${esc(trunc(evt.summary,30))}</div>`;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
html += `</div></div>`;
|
|
707
|
+
return html;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function toggleSeqGroup(header) {
|
|
711
|
+
header.classList.toggle('expanded');
|
|
712
|
+
header.nextElementSibling.classList.toggle('expanded');
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// ========================================
|
|
716
|
+
// TIMELINE VIEW
|
|
717
|
+
// ========================================
|
|
718
|
+
function renderTimelineView() {
|
|
719
|
+
const container = document.getElementById('timeline-view');
|
|
720
|
+
const filtered = getFilteredEvents();
|
|
721
|
+
|
|
722
|
+
let html = '';
|
|
723
|
+
filtered.forEach((evt, fi) => {
|
|
724
|
+
const idx = events.indexOf(evt);
|
|
725
|
+
const isActive = idx === selectedEventIdx;
|
|
726
|
+
const col = actorColors[evt.from]||'#667';
|
|
727
|
+
const indent = evt.depth * 16;
|
|
728
|
+
const typeBadge = evt.type === 'delegation' || evt.type === 'delegation_return' ? '<span class="tl-type-badge delegation">deleg</span>'
|
|
729
|
+
: evt.isStructural ? '<span class="tl-type-badge structural">'+esc(evt.structuralType||'struct')+'</span>'
|
|
730
|
+
: (evt.type === 'tool_call' || evt.type === 'tool_result') ? '<span class="tl-type-badge tool">'+ esc(evt.toolName||'tool')+'</span>' : '';
|
|
731
|
+
|
|
732
|
+
html += `<div class="tl-event${isActive?' active':''}${evt.isStructural?' structural':''}" data-idx="${idx}" onclick="selectEvent(${idx})">`;
|
|
733
|
+
html += `<div class="tl-idx">${idx}</div>`;
|
|
734
|
+
html += `<div class="tl-time">${fmtTime(evt.ts)}</div>`;
|
|
735
|
+
html += `<div class="tl-actor-dot" style="background:${col};margin-left:${indent}px"></div>`;
|
|
736
|
+
html += `<div class="tl-body">`;
|
|
737
|
+
html += `<div class="tl-header-line">`;
|
|
738
|
+
html += `<span class="tl-from" style="color:${col}">${esc(evt.from)}</span>`;
|
|
739
|
+
html += `<span class="tl-arrow">\u2192</span>`;
|
|
740
|
+
html += `<span class="tl-to" style="color:${actorColors[evt.to]||'#667'}">${esc(evt.to)}</span>`;
|
|
741
|
+
html += typeBadge;
|
|
742
|
+
html += `</div>`;
|
|
743
|
+
if (evt.summary && evt.type !== 'tool_result') {
|
|
744
|
+
html += `<div class="tl-preview">${esc(evt.summary)}</div>`;
|
|
745
|
+
}
|
|
746
|
+
if (evt.usage) {
|
|
747
|
+
html += `<div class="tl-usage">${fmtNum(evt.usage.input_tokens||0)} in / ${fmtNum(evt.usage.output_tokens||0)} out</div>`;
|
|
748
|
+
}
|
|
749
|
+
html += `</div></div>`;
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
container.innerHTML = html || '<div class="detail-empty"><div>No events match</div></div>';
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// ========================================
|
|
756
|
+
// DETAIL VIEW
|
|
757
|
+
// ========================================
|
|
758
|
+
function selectEvent(idx) {
|
|
759
|
+
selectedEventIdx = idx;
|
|
760
|
+
const evt = events[idx];
|
|
761
|
+
if (!evt) return;
|
|
762
|
+
|
|
763
|
+
// Determine which session to show
|
|
764
|
+
const sessionId = evt.sessionId;
|
|
765
|
+
selectedSessionId = sessionId;
|
|
766
|
+
|
|
767
|
+
// Highlight in sequence/timeline
|
|
768
|
+
document.querySelectorAll('.seq-row,.tl-event').forEach(el => {
|
|
769
|
+
el.classList.toggle('active', parseInt(el.dataset.idx) === idx);
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
renderDetail();
|
|
773
|
+
|
|
774
|
+
// Scroll the active row into view in the current view
|
|
775
|
+
const activeRow = document.querySelector(activeView === 'sequence' ? '.seq-row.active' : '.tl-event.active');
|
|
776
|
+
if (activeRow) activeRow.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function renderDetail() {
|
|
780
|
+
const container = document.getElementById('detail-view');
|
|
781
|
+
if (!selectedSessionId) {
|
|
782
|
+
container.innerHTML = '<div class="detail-empty"><div class="icon">←</div><div>Select an event</div></div>';
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const node = allNodes[selectedSessionId];
|
|
787
|
+
if (!node) { container.innerHTML = '<div class="detail-empty"><div>Session not found</div></div>'; return; }
|
|
788
|
+
|
|
789
|
+
const session = node.session||{};
|
|
790
|
+
const type = session.thread_type||'root';
|
|
791
|
+
const usage = sumUsageTree(node);
|
|
792
|
+
const messages = node.messages||[];
|
|
793
|
+
|
|
794
|
+
// Build breadcrumbs
|
|
795
|
+
const crumbs = buildBreadcrumbs(selectedSessionId);
|
|
796
|
+
|
|
797
|
+
let html = '';
|
|
798
|
+
|
|
799
|
+
// Breadcrumbs
|
|
800
|
+
if (crumbs.length > 1) {
|
|
801
|
+
html += '<div class="breadcrumbs">';
|
|
802
|
+
crumbs.forEach((c, i) => {
|
|
803
|
+
if (i > 0) html += '<span class="breadcrumb-sep">/</span>';
|
|
804
|
+
const isCurrent = c.id === selectedSessionId;
|
|
805
|
+
html += `<span class="breadcrumb${isCurrent?' current':''}" onclick="${isCurrent?'':`navigateToSession('${c.id}')`}">${esc(c.name)}</span>`;
|
|
806
|
+
});
|
|
807
|
+
html += '</div>';
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Header
|
|
811
|
+
html += `<div class="detail-header">`;
|
|
812
|
+
html += `<div style="display:flex;align-items:center">`;
|
|
813
|
+
html += `<span class="po-name">${esc(session.po_name||'?')}</span>`;
|
|
814
|
+
html += `<span class="type-badge badge-${type}">${type}</span>`;
|
|
815
|
+
if (session.parent_po) html += `<span class="parent-info">from <em>${esc(session.parent_po)}</em></span>`;
|
|
816
|
+
html += `</div>`;
|
|
817
|
+
html += `<div class="stats"><span><span class="v">${fmtNum(usage.input)}</span> in</span><span><span class="v">${fmtNum(usage.output)}</span> out</span></div>`;
|
|
818
|
+
html += `</div>`;
|
|
819
|
+
|
|
820
|
+
// Messages
|
|
821
|
+
html += '<div class="messages">';
|
|
822
|
+
|
|
823
|
+
// tool_call_id -> result lookup
|
|
824
|
+
const toolResults = {};
|
|
825
|
+
messages.forEach(m => {
|
|
826
|
+
if (m.role === 'tool' && m.tool_results) m.tool_results.forEach(tr => { toolResults[tr.tool_call_id] = tr; });
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// delegation children lookup
|
|
830
|
+
const delegKids = {};
|
|
831
|
+
(node.children||[]).forEach(c => {
|
|
832
|
+
const cp = c.session?.po_name;
|
|
833
|
+
if (c.session?.thread_type === 'delegation' && cp) {
|
|
834
|
+
if (!delegKids[cp]) delegKids[cp] = [];
|
|
835
|
+
delegKids[cp].push(c);
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
for (const msg of messages) {
|
|
840
|
+
if (msg.role === 'tool') continue;
|
|
841
|
+
|
|
842
|
+
if (msg.role === 'user') {
|
|
843
|
+
const fromPo = msg.from_po && msg.from_po !== 'human';
|
|
844
|
+
html += `<div class="msg user${fromPo?' from-po':''}">`;
|
|
845
|
+
html += `<div class="msg-head"><span class="msg-sender ${fromPo?'delegator':'human'}">${esc(fromPo?msg.from_po:'human')}</span>`;
|
|
846
|
+
html += `<span class="msg-ts">${fmtTime(msg.created_at)}</span></div>`;
|
|
847
|
+
html += `<div class="msg-body">${esc(msg.content||'')}</div></div>`;
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
if (msg.role === 'assistant') {
|
|
851
|
+
if (msg.content) {
|
|
852
|
+
html += `<div class="msg assistant">`;
|
|
853
|
+
html += `<div class="msg-head"><span class="msg-sender assistant">${esc(session.po_name||'assistant')}</span>`;
|
|
854
|
+
html += `<span class="msg-ts">${fmtTime(msg.created_at)}</span>`;
|
|
855
|
+
if (msg.usage) html += `<span class="msg-tokens">${fmtNum(msg.usage.input_tokens||0)} in / ${fmtNum(msg.usage.output_tokens||0)} out</span>`;
|
|
856
|
+
html += `</div>`;
|
|
857
|
+
html += `<div class="msg-body">${esc(msg.content)}</div></div>`;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (msg.tool_calls) {
|
|
861
|
+
for (const tc of msg.tool_calls) {
|
|
862
|
+
const tcName = tc.name||'';
|
|
863
|
+
const tcArgs = tc.arguments||{};
|
|
864
|
+
const tcId = tc.id||'';
|
|
865
|
+
const result = toolResults[tcId];
|
|
866
|
+
const isSt = STRUCTURAL_TOOLS.has(tcName);
|
|
867
|
+
|
|
868
|
+
html += `<div class="tc">`;
|
|
869
|
+
html += `<div class="tc-head" onclick="toggleTc(this)">`;
|
|
870
|
+
html += `<span class="chv">▶</span>`;
|
|
871
|
+
html += `<span class="tc-name" ${isSt?'style="color:var(--red)"':''}>${esc(tcName)}</span>`;
|
|
872
|
+
html += `<span class="tc-label">${isSt?'structural':'tool call'}</span>`;
|
|
873
|
+
html += `</div>`;
|
|
874
|
+
html += `<div class="tc-body">`;
|
|
875
|
+
html += `<div class="tc-section"><div class="tc-section-label">Arguments</div><pre>${esc(JSON.stringify(tcArgs,null,2))}</pre></div>`;
|
|
876
|
+
if (result) html += `<div class="tc-section"><div class="tc-section-label">Result</div><div class="tc-result">${esc(result.content||'')}</div></div>`;
|
|
877
|
+
html += `</div></div>`;
|
|
878
|
+
|
|
879
|
+
// Delegation marker
|
|
880
|
+
if (delegKids[tcName] && delegKids[tcName].length > 0) {
|
|
881
|
+
const child = delegKids[tcName].shift();
|
|
882
|
+
if (child) {
|
|
883
|
+
const cid = child.session?.id;
|
|
884
|
+
const cMsgs = (child.messages||[]).filter(m=>m.role!=='tool').length;
|
|
885
|
+
html += `<div class="deleg-marker" onclick="navigateToSession('${cid}')">`;
|
|
886
|
+
html += `<span>\u2192</span> <strong>Delegated to ${esc(tcName)}</strong>`;
|
|
887
|
+
html += `<span class="dm-info">${cMsgs} messages</span>`;
|
|
888
|
+
html += `</div>`;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
html += '</div>';
|
|
897
|
+
container.innerHTML = html;
|
|
898
|
+
container.scrollTop = 0;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function toggleTc(head) {
|
|
902
|
+
head.classList.toggle('open');
|
|
903
|
+
head.nextElementSibling.classList.toggle('open');
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function navigateToSession(sessionId) {
|
|
907
|
+
selectedSessionId = sessionId;
|
|
908
|
+
// Find first event for this session
|
|
909
|
+
const idx = events.findIndex(e => e.sessionId === sessionId);
|
|
910
|
+
if (idx >= 0) selectedEventIdx = idx;
|
|
911
|
+
renderDetail();
|
|
912
|
+
// Highlight in views
|
|
913
|
+
document.querySelectorAll('.seq-row,.tl-event').forEach(el => {
|
|
914
|
+
el.classList.toggle('active', parseInt(el.dataset.idx) === selectedEventIdx);
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function buildBreadcrumbs(sessionId) {
|
|
919
|
+
const crumbs = [];
|
|
920
|
+
let current = allNodes[sessionId];
|
|
921
|
+
while (current) {
|
|
922
|
+
const s = current.session||{};
|
|
923
|
+
crumbs.unshift({ id: s.id, name: s.po_name||'?' });
|
|
924
|
+
// Find parent
|
|
925
|
+
if (s.parent_session_id && allNodes[s.parent_session_id]) {
|
|
926
|
+
current = allNodes[s.parent_session_id];
|
|
927
|
+
} else if (current._parent) {
|
|
928
|
+
current = current._parent;
|
|
929
|
+
// Check if this is really the parent by matching session
|
|
930
|
+
const ps = current.session;
|
|
931
|
+
if (ps && ps.id && !crumbs.find(c=>c.id===ps.id)) {
|
|
932
|
+
continue;
|
|
933
|
+
} else break;
|
|
934
|
+
} else break;
|
|
935
|
+
}
|
|
936
|
+
return crumbs;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// --- Utility ---
|
|
940
|
+
function sumUsageTree(node) {
|
|
941
|
+
let input=0, output=0;
|
|
942
|
+
(node.messages||[]).forEach(m => { if(m.usage){ input+=m.usage.input_tokens||0; output+=m.usage.output_tokens||0; }});
|
|
943
|
+
(node.children||[]).forEach(c => { const u=sumUsageTree(c); input+=u.input; output+=u.output; });
|
|
944
|
+
return {input,output};
|
|
945
|
+
}
|
|
946
|
+
function fmtNum(n) { if(n>=1e6) return (n/1e6).toFixed(1)+'M'; if(n>=1e3) return (n/1e3).toFixed(1)+'K'; return ''+n; }
|
|
947
|
+
function fmtTime(iso) { if(!iso) return ''; try { return new Date(iso).toLocaleTimeString([],{hour:'2-digit',minute:'2-digit',second:'2-digit'}); } catch{return '';} }
|
|
948
|
+
function trunc(s,n) { return s && s.length>n ? s.substring(0,n)+'...' : (s||''); }
|
|
949
|
+
function esc(s) { if(!s) return ''; const d=document.createElement('div'); d.textContent=s; return d.innerHTML; }
|
|
950
|
+
|
|
951
|
+
// ========================================
|
|
952
|
+
// SAMPLE DATA
|
|
953
|
+
// ========================================
|
|
954
|
+
function generateSampleData() {
|
|
955
|
+
const t = n => new Date(Date.now() - n*1000).toISOString();
|
|
956
|
+
return {
|
|
957
|
+
session: { id:"s1", po_name:"solver", name:"Analyze data pipeline", thread_type:"root", parent_po:null, created_at:t(600) },
|
|
958
|
+
messages: [
|
|
959
|
+
{ role:"user", content:"Analyze the data pipeline in /src/pipeline and fix the ETL issue.", from_po:null, created_at:t(600) },
|
|
960
|
+
{ role:"assistant", content:null,
|
|
961
|
+
tool_calls:[{id:"c1",name:"reader",arguments:{message:"Read and summarize all files in /src/pipeline/"}}],
|
|
962
|
+
usage:{input_tokens:1250,output_tokens:45,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(595) },
|
|
963
|
+
{ role:"tool", tool_results:[{tool_call_id:"c1",name:"reader",content:"3 stages: extract (PostgreSQL), transform (cleaning), load (warehouse). Key issue: transform.py drops ~12% of rows with null timestamps."}], created_at:t(560) },
|
|
964
|
+
{ role:"assistant", content:null,
|
|
965
|
+
tool_calls:[{id:"c2",name:"read_file",arguments:{path:"/src/pipeline/transform.py"}}],
|
|
966
|
+
usage:{input_tokens:1800,output_tokens:32,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(555) },
|
|
967
|
+
{ role:"tool", tool_results:[{tool_call_id:"c2",name:"read_file",content:"import pandas as pd\nfrom datetime import datetime\n\ndef transform(df):\n df = df.dropna(subset=['created_at'])\n df['created_at'] = pd.to_datetime(df['created_at'])\n df['amount'] = df['amount'].clip(lower=0)\n df['status'] = df['status'].str.lower()\n return df"}], created_at:t(553) },
|
|
968
|
+
{ role:"assistant", content:null,
|
|
969
|
+
tool_calls:[{id:"c3",name:"write_file",arguments:{path:"/src/pipeline/transform.py",content:"import pandas as pd\nfrom datetime import datetime\n\ndef transform(df):\n df['created_at'] = df['created_at'].fillna(df['updated_at'])\n df['created_at'] = pd.to_datetime(df['created_at'])\n df['amount'] = df['amount'].clip(lower=0)\n df['status'] = df['status'].str.lower()\n return df"}}],
|
|
970
|
+
usage:{input_tokens:2200,output_tokens:60,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(545) },
|
|
971
|
+
{ role:"tool", tool_results:[{tool_call_id:"c3",name:"write_file",content:"File written successfully"}], created_at:t(543) },
|
|
972
|
+
{ role:"assistant", content:null,
|
|
973
|
+
tool_calls:[{id:"c4",name:"validator",arguments:{message:"Run the test suite for /src/pipeline/ and verify the transform fix handles null timestamps correctly."}}],
|
|
974
|
+
usage:{input_tokens:2500,output_tokens:55,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(540) },
|
|
975
|
+
{ role:"tool", tool_results:[{tool_call_id:"c4",name:"validator",content:"All 12 tests pass. Null timestamp handling verified: 0 rows dropped, fallback to updated_at working correctly."}], created_at:t(500) },
|
|
976
|
+
{ role:"assistant", content:null,
|
|
977
|
+
tool_calls:[{id:"c5",name:"add_capability",arguments:{name:"solver",capability:"write_file"}}],
|
|
978
|
+
usage:{input_tokens:2800,output_tokens:30,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(495) },
|
|
979
|
+
{ role:"tool", tool_results:[{tool_call_id:"c5",name:"add_capability",content:"Capability write_file added to solver"}], created_at:t(494) },
|
|
980
|
+
{ role:"assistant", content:"Fixed the ETL pipeline issue.\n\n**Problem:** `transform.py` used `df.dropna(subset=['created_at'])` which silently dropped ~12% of records with null timestamps.\n\n**Fix:** Replaced with `df['created_at'].fillna(df['updated_at'])` to use the `updated_at` field as a fallback instead of discarding rows.\n\n**Verification:** All 12 tests pass. The validator confirmed 0 rows are now dropped and the fallback works correctly.\n\nI've also added `write_file` to my capabilities for future code fixes.",
|
|
981
|
+
usage:{input_tokens:3000,output_tokens:120,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(490) }
|
|
982
|
+
],
|
|
983
|
+
children: [
|
|
984
|
+
{
|
|
985
|
+
session: { id:"s2", po_name:"reader", name:"Read pipeline files", thread_type:"delegation", parent_po:"solver", created_at:t(595) },
|
|
986
|
+
messages: [
|
|
987
|
+
{ role:"user", content:"Read and summarize all files in /src/pipeline/", from_po:"solver", created_at:t(595) },
|
|
988
|
+
{ role:"assistant", content:null,
|
|
989
|
+
tool_calls:[{id:"r1",name:"list_files",arguments:{path:"/src/pipeline/"}}],
|
|
990
|
+
usage:{input_tokens:420,output_tokens:18,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(593) },
|
|
991
|
+
{ role:"tool", tool_results:[{tool_call_id:"r1",name:"list_files",content:"extract.py\ntransform.py\nload.py\nconfig.yml\n__init__.py"}], created_at:t(592) },
|
|
992
|
+
{ role:"assistant", content:null,
|
|
993
|
+
tool_calls:[
|
|
994
|
+
{id:"r2",name:"read_file",arguments:{path:"/src/pipeline/extract.py"}},
|
|
995
|
+
{id:"r3",name:"read_file",arguments:{path:"/src/pipeline/transform.py"}},
|
|
996
|
+
{id:"r4",name:"read_file",arguments:{path:"/src/pipeline/load.py"}}
|
|
997
|
+
],
|
|
998
|
+
usage:{input_tokens:680,output_tokens:42,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(590) },
|
|
999
|
+
{ role:"tool", tool_results:[
|
|
1000
|
+
{tool_call_id:"r2",name:"read_file",content:"import psycopg2\n\ndef extract(config):\n conn = psycopg2.connect(**config['db'])\n cursor = conn.cursor()\n cursor.execute('SELECT * FROM events ORDER BY id')\n while True:\n rows = cursor.fetchmany(1000)\n if not rows: break\n yield rows"},
|
|
1001
|
+
{tool_call_id:"r3",name:"read_file",content:"import pandas as pd\nfrom datetime import datetime\n\ndef transform(df):\n df = df.dropna(subset=['created_at'])\n df['created_at'] = pd.to_datetime(df['created_at'])\n df['amount'] = df['amount'].clip(lower=0)\n df['status'] = df['status'].str.lower()\n return df"},
|
|
1002
|
+
{tool_call_id:"r4",name:"read_file",content:"from sqlalchemy import create_engine\n\ndef load(df, config):\n engine = create_engine(config['warehouse_url'])\n df.to_sql('events_clean', engine, if_exists='append', index=False, method='multi', chunksize=500)"}
|
|
1003
|
+
], created_at:t(585) },
|
|
1004
|
+
{ role:"assistant",
|
|
1005
|
+
content:"3 stages: extract (PostgreSQL), transform (cleaning), load (warehouse). Key issue: transform.py drops ~12% of rows with null timestamps.",
|
|
1006
|
+
usage:{input_tokens:1400,output_tokens:95,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(565) }
|
|
1007
|
+
],
|
|
1008
|
+
children: []
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
session: { id:"s3", po_name:"validator", name:"Validate fix", thread_type:"delegation", parent_po:"solver", created_at:t(540) },
|
|
1012
|
+
messages: [
|
|
1013
|
+
{ role:"user", content:"Run the test suite for /src/pipeline/ and verify the transform fix handles null timestamps correctly.", from_po:"solver", created_at:t(540) },
|
|
1014
|
+
{ role:"assistant", content:null,
|
|
1015
|
+
tool_calls:[{id:"v1",name:"list_files",arguments:{path:"/src/pipeline/tests/"}}],
|
|
1016
|
+
usage:{input_tokens:350,output_tokens:15,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(538) },
|
|
1017
|
+
{ role:"tool", tool_results:[{tool_call_id:"v1",name:"list_files",content:"test_extract.py\ntest_transform.py\ntest_load.py\nconftest.py"}], created_at:t(537) },
|
|
1018
|
+
{ role:"assistant", content:null,
|
|
1019
|
+
tool_calls:[{id:"v2",name:"read_file",arguments:{path:"/src/pipeline/tests/test_transform.py"}}],
|
|
1020
|
+
usage:{input_tokens:500,output_tokens:20,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(535) },
|
|
1021
|
+
{ role:"tool", tool_results:[{tool_call_id:"v2",name:"read_file",content:"import pytest\nimport pandas as pd\nfrom pipeline.transform import transform\n\ndef test_null_timestamps():\n df = pd.DataFrame({'created_at': [None, '2024-01-01'], 'updated_at': ['2024-02-01', '2024-01-01'], 'amount': [100, 200], 'status': ['ACTIVE', 'inactive']})\n result = transform(df)\n assert len(result) == 2\n assert result.iloc[0]['created_at'] == pd.Timestamp('2024-02-01')"}], created_at:t(533) },
|
|
1022
|
+
{ role:"assistant", content:null,
|
|
1023
|
+
tool_calls:[
|
|
1024
|
+
{id:"v3",name:"read_file",arguments:{path:"/src/pipeline/transform.py"}},
|
|
1025
|
+
{id:"v4",name:"read_file",arguments:{path:"/src/pipeline/tests/conftest.py"}}
|
|
1026
|
+
],
|
|
1027
|
+
usage:{input_tokens:900,output_tokens:28,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(520) },
|
|
1028
|
+
{ role:"tool", tool_results:[
|
|
1029
|
+
{tool_call_id:"v3",name:"read_file",content:"import pandas as pd\nfrom datetime import datetime\n\ndef transform(df):\n df['created_at'] = df['created_at'].fillna(df['updated_at'])\n df['created_at'] = pd.to_datetime(df['created_at'])\n df['amount'] = df['amount'].clip(lower=0)\n df['status'] = df['status'].str.lower()\n return df"},
|
|
1030
|
+
{tool_call_id:"v4",name:"read_file",content:"import pytest\n\n@pytest.fixture\ndef sample_data():\n return {'events': [{'id': 1, 'created_at': None, 'updated_at': '2024-01-15'}]}"}
|
|
1031
|
+
], created_at:t(515) },
|
|
1032
|
+
{ role:"assistant",
|
|
1033
|
+
content:"All 12 tests pass. Null timestamp handling verified: 0 rows dropped, fallback to updated_at working correctly.",
|
|
1034
|
+
usage:{input_tokens:1200,output_tokens:40,model:"claude-sonnet-4-5",provider:"anthropic"}, created_at:t(505) }
|
|
1035
|
+
],
|
|
1036
|
+
children: []
|
|
1037
|
+
}
|
|
1038
|
+
]
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
</script>
|
|
1042
|
+
</body>
|
|
1043
|
+
</html>
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: prompt_objects
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Scott Werner
|
|
@@ -336,6 +336,7 @@ files:
|
|
|
336
336
|
- templates/writer/objects/coordinator.md
|
|
337
337
|
- templates/writer/objects/editor.md
|
|
338
338
|
- templates/writer/objects/researcher.md
|
|
339
|
+
- tools/thread-explorer.html
|
|
339
340
|
homepage: https://github.com/works-on-your-machine/prompt_objects
|
|
340
341
|
licenses:
|
|
341
342
|
- MIT
|