kward 0.69.0 → 0.70.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 +28 -0
- data/Gemfile.lock +2 -2
- data/doc/configuration.md +1 -1
- data/doc/getting-started.md +3 -1
- data/doc/usage.md +9 -2
- data/lib/kward/cli/prompt_interface.rb +5 -1
- data/lib/kward/cli/sessions.rb +197 -7
- data/lib/kward/cli/slash_commands.rb +16 -7
- data/lib/kward/cli.rb +1 -0
- data/lib/kward/conversation.rb +15 -0
- data/lib/kward/model/model_info.rb +9 -2
- data/lib/kward/prompt_interface/overlay_renderer.rb +24 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +13 -11
- data/lib/kward/prompt_interface/screen.rb +9 -4
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -9
- data/lib/kward/prompt_interface/slash_overlay.rb +4 -4
- data/lib/kward/prompt_interface.rb +9 -4
- data/lib/kward/prompts/commands.rb +4 -2
- data/lib/kward/rpc/session_manager.rb +2 -2
- data/lib/kward/rpc/session_tree_rows.rb +54 -13
- data/lib/kward/session_diff.rb +106 -9
- data/lib/kward/session_store.rb +5 -1
- data/lib/kward/session_tree_renderer.rb +56 -14
- data/lib/kward/version.rb +1 -1
- data/templates/default/fulldoc/html/css/kward.css +314 -71
- data/templates/default/fulldoc/html/js/kward.js +100 -97
- data/templates/default/layout/html/layout.erb +21 -6
- data/templates/default/layout/html/setup.rb +1 -1
- metadata +1 -1
|
@@ -131,7 +131,7 @@ module Kward
|
|
|
131
131
|
@banner = Banner.new(message: banner_message, pixels: banner_pixels, screen_height: method(:screen_height))
|
|
132
132
|
end
|
|
133
133
|
|
|
134
|
-
def start
|
|
134
|
+
def start(render: true)
|
|
135
135
|
@mutex.synchronize do
|
|
136
136
|
return if @started
|
|
137
137
|
|
|
@@ -140,7 +140,7 @@ module Kward
|
|
|
140
140
|
@asking = true
|
|
141
141
|
@output_io.print(KEYBOARD_PROTOCOL_ENABLE)
|
|
142
142
|
@output_io.print(BRACKETED_PASTE_ENABLE)
|
|
143
|
-
render_prompt_locked
|
|
143
|
+
render_prompt_locked if render
|
|
144
144
|
end
|
|
145
145
|
end
|
|
146
146
|
|
|
@@ -198,9 +198,10 @@ module Kward
|
|
|
198
198
|
end
|
|
199
199
|
|
|
200
200
|
def restore_transcript
|
|
201
|
+
start(render: false) unless @started
|
|
201
202
|
@mutex.synchronize do
|
|
202
|
-
clear_prompt_for_output_locked
|
|
203
203
|
@output_io.print(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
204
|
+
clear_prompt_for_output_locked unless @rendered_rows.zero? && @last_composer_rows.empty?
|
|
204
205
|
@transcript_buffer.clear
|
|
205
206
|
@visual_banner_count = 0
|
|
206
207
|
@transcript_viewport_rows = 0
|
|
@@ -213,9 +214,9 @@ module Kward
|
|
|
213
214
|
ensure
|
|
214
215
|
@mutex.synchronize do
|
|
215
216
|
@restoring_transcript = false
|
|
216
|
-
@output_io.print(SYNCHRONIZED_OUTPUT_DISABLE)
|
|
217
217
|
width, height = screen_size
|
|
218
218
|
redraw_screen_locked(width: width, height: height)
|
|
219
|
+
@output_io.print(SYNCHRONIZED_OUTPUT_DISABLE)
|
|
219
220
|
@output_io.flush
|
|
220
221
|
end
|
|
221
222
|
end
|
|
@@ -270,6 +271,10 @@ module Kward
|
|
|
270
271
|
answer.start_with?("y")
|
|
271
272
|
end
|
|
272
273
|
|
|
274
|
+
def picker_choice_width
|
|
275
|
+
[overlay_card_width(screen_width) - 6, 1].max
|
|
276
|
+
end
|
|
277
|
+
|
|
273
278
|
def select(message, choices, title: "Sessions", custom: false, initial_index: 0)
|
|
274
279
|
return nil if choices.empty? && !custom
|
|
275
280
|
|
|
@@ -8,10 +8,12 @@ module Kward
|
|
|
8
8
|
{ name: "exit", description: "Exit the interactive session.", argument_hint: "" },
|
|
9
9
|
{ name: "quit", description: "Exit the interactive session.", argument_hint: "" },
|
|
10
10
|
{ name: "new", description: "Start a new session.", argument_hint: "" },
|
|
11
|
-
{ name: "
|
|
11
|
+
{ name: "sessions", description: "Open the saved sessions picker.", argument_hint: "[path]" },
|
|
12
|
+
{ name: "resume", description: "Alias for /sessions.", argument_hint: "[path]" },
|
|
12
13
|
{ name: "name", description: "Name or clear the current session.", argument_hint: "[name]" },
|
|
13
14
|
{ name: "clone", description: "Clone the current session.", argument_hint: "" },
|
|
14
|
-
{ name: "
|
|
15
|
+
{ name: "rewind", description: "Revisit an earlier prompt and try a different direction.", argument_hint: "" },
|
|
16
|
+
{ name: "tree", description: "Inspect and navigate the full technical session tree.", argument_hint: "" },
|
|
15
17
|
{ name: "copy", description: "Copy clean session text to the clipboard.", argument_hint: "[last|transcript]" },
|
|
16
18
|
{ name: "export", description: "Export the current session as Markdown.", argument_hint: "[path]" },
|
|
17
19
|
{ name: "compact", description: "Compact the current conversation context.", argument_hint: "[instructions]" },
|
|
@@ -251,8 +251,8 @@ module Kward
|
|
|
251
251
|
if summarize
|
|
252
252
|
summary = summarize_branch(rpc_session, from_id: previous_leaf, to_id: target_leaf, custom_instructions: custom_instructions)
|
|
253
253
|
target_leaf = rpc_session.session.append_branch_summary(target_leaf, from_id: previous_leaf, summary: summary, details: {})
|
|
254
|
-
|
|
255
|
-
|
|
254
|
+
elsif target_leaf
|
|
255
|
+
rpc_session.session.branch(target_leaf)
|
|
256
256
|
end
|
|
257
257
|
|
|
258
258
|
reload_rpc_session(rpc_session)
|
|
@@ -34,7 +34,12 @@ module Kward
|
|
|
34
34
|
multiple_roots = visible_roots.length > 1
|
|
35
35
|
result = []
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
stack = visible_roots.sort_by { |root| tree_contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index.map do |root, index|
|
|
38
|
+
[root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots]
|
|
39
|
+
end.reverse
|
|
40
|
+
|
|
41
|
+
until stack.empty?
|
|
42
|
+
node, indent, just_branched, show_connector, is_last, gutters, virtual_root_child = stack.pop
|
|
38
43
|
entry = node[:source]["entry"] || {}
|
|
39
44
|
entry_id = entry["id"].to_s
|
|
40
45
|
formatted = tree_entry_display(entry, tool_calls_by_id)
|
|
@@ -66,14 +71,10 @@ module Kward
|
|
|
66
71
|
end
|
|
67
72
|
connector_position = [display_indent - 1, 0].max
|
|
68
73
|
child_gutters = show_connector && !virtual_root_child ? gutters + [{ position: connector_position, show: !is_last }] : gutters
|
|
69
|
-
children.each_with_index do |child, index|
|
|
70
|
-
|
|
74
|
+
children.each_with_index.reverse_each do |child, index|
|
|
75
|
+
stack << [child, child_indent, multiple_children, multiple_children, index == children.length - 1, child_gutters, false]
|
|
71
76
|
end
|
|
72
77
|
end
|
|
73
|
-
|
|
74
|
-
visible_roots.sort_by { |root| tree_contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index do |root, index|
|
|
75
|
-
walk.call(root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots)
|
|
76
|
-
end
|
|
77
78
|
result
|
|
78
79
|
end
|
|
79
80
|
|
|
@@ -83,7 +84,9 @@ module Kward
|
|
|
83
84
|
by_id = tree_entries_by_id(roots)
|
|
84
85
|
ids = []
|
|
85
86
|
current = by_id[leaf_id.to_s]
|
|
86
|
-
|
|
87
|
+
seen = {}
|
|
88
|
+
while current && !seen[current["id"].to_s]
|
|
89
|
+
seen[current["id"].to_s] = true
|
|
87
90
|
ids << current["id"].to_s
|
|
88
91
|
current = by_id[current["parentId"].to_s]
|
|
89
92
|
end
|
|
@@ -93,8 +96,12 @@ module Kward
|
|
|
93
96
|
def tree_entries_by_id(roots)
|
|
94
97
|
roots.each_with_object({}) do |root, map|
|
|
95
98
|
stack = [root]
|
|
99
|
+
seen = {}
|
|
96
100
|
until stack.empty?
|
|
97
101
|
node = stack.pop
|
|
102
|
+
next if seen[node.object_id]
|
|
103
|
+
|
|
104
|
+
seen[node.object_id] = true
|
|
98
105
|
entry = node["entry"] || {}
|
|
99
106
|
map[entry["id"].to_s] = entry unless entry["id"].to_s.empty?
|
|
100
107
|
stack.concat(Array(node["children"]))
|
|
@@ -103,10 +110,29 @@ module Kward
|
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
def visible_tree_nodes(node)
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
results = {}
|
|
114
|
+
stack = [[node, false, {}]]
|
|
115
|
+
|
|
116
|
+
until stack.empty?
|
|
117
|
+
current, visited, seen = stack.pop
|
|
118
|
+
node_key = current.object_id
|
|
119
|
+
next if seen[node_key]
|
|
120
|
+
|
|
121
|
+
if visited
|
|
122
|
+
children = Array(current["children"]).flat_map { |child| results[child.object_id] || [] }
|
|
123
|
+
results[node_key] = if hidden_tree_entry?(current["entry"] || {})
|
|
124
|
+
children
|
|
125
|
+
else
|
|
126
|
+
[{ source: current, children: children }]
|
|
127
|
+
end
|
|
128
|
+
else
|
|
129
|
+
branch_seen = seen.merge(node_key => true)
|
|
130
|
+
stack << [current, true, seen]
|
|
131
|
+
Array(current["children"]).reverse_each { |child| stack << [child, false, branch_seen] unless branch_seen[child.object_id] }
|
|
132
|
+
end
|
|
133
|
+
end
|
|
108
134
|
|
|
109
|
-
[
|
|
135
|
+
results[node.object_id] || []
|
|
110
136
|
end
|
|
111
137
|
|
|
112
138
|
def hidden_tree_entry?(entry)
|
|
@@ -126,15 +152,30 @@ module Kward
|
|
|
126
152
|
end
|
|
127
153
|
|
|
128
154
|
def tree_contains_active_path?(node, active_path)
|
|
129
|
-
|
|
130
|
-
|
|
155
|
+
stack = [node]
|
|
156
|
+
seen = {}
|
|
157
|
+
until stack.empty?
|
|
158
|
+
current = stack.pop
|
|
159
|
+
next if seen[current.object_id]
|
|
160
|
+
|
|
161
|
+
seen[current.object_id] = true
|
|
162
|
+
entry_id = (current[:source]["entry"] || {})["id"].to_s
|
|
163
|
+
return true if active_path.include?(entry_id)
|
|
164
|
+
|
|
165
|
+
stack.concat(current[:children])
|
|
166
|
+
end
|
|
167
|
+
false
|
|
131
168
|
end
|
|
132
169
|
|
|
133
170
|
def tree_tool_calls(roots)
|
|
134
171
|
roots.each_with_object({}) do |root, tool_calls_by_id|
|
|
135
172
|
stack = [root]
|
|
173
|
+
seen = {}
|
|
136
174
|
until stack.empty?
|
|
137
175
|
node = stack.pop
|
|
176
|
+
next if seen[node.object_id]
|
|
177
|
+
|
|
178
|
+
seen[node.object_id] = true
|
|
138
179
|
entry = node["entry"] || {}
|
|
139
180
|
message = entry["message"]
|
|
140
181
|
if entry["type"] == "message" && message.is_a?(Hash) && MessageAccess.role(message) == "assistant"
|
data/lib/kward/session_diff.rb
CHANGED
|
@@ -4,11 +4,18 @@ require "json"
|
|
|
4
4
|
module Kward
|
|
5
5
|
# Counts unified-diff additions and deletions for summaries.
|
|
6
6
|
class SessionDiff
|
|
7
|
-
attr_reader :additions, :deletions
|
|
8
|
-
|
|
9
7
|
def initialize(additions: 0, deletions: 0)
|
|
10
|
-
@
|
|
11
|
-
@
|
|
8
|
+
@base_additions = additions.to_i
|
|
9
|
+
@base_deletions = deletions.to_i
|
|
10
|
+
@file_changes = Hash.new { |changes, path| changes[path] = { removed: [], added: [] } }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def additions
|
|
14
|
+
@base_additions + @file_changes.values.sum { |changes| changes[:added].length }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deletions
|
|
18
|
+
@base_deletions + @file_changes.values.sum { |changes| changes[:removed].length }
|
|
12
19
|
end
|
|
13
20
|
|
|
14
21
|
def self.from_session_file(path)
|
|
@@ -65,7 +72,7 @@ module Kward
|
|
|
65
72
|
end
|
|
66
73
|
|
|
67
74
|
def empty?
|
|
68
|
-
|
|
75
|
+
additions.zero? && deletions.zero?
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
def add_tool_result(content)
|
|
@@ -76,11 +83,19 @@ module Kward
|
|
|
76
83
|
end
|
|
77
84
|
|
|
78
85
|
def add_diff(diff)
|
|
79
|
-
|
|
80
|
-
|
|
86
|
+
if self.class.truncated_diff_stats(diff) || self.class.truncated_diff?(diff)
|
|
87
|
+
counts = self.class.count(diff)
|
|
88
|
+
return false if counts[:additions].zero? && counts[:deletions].zero?
|
|
81
89
|
|
|
82
|
-
|
|
83
|
-
|
|
90
|
+
@base_additions += counts[:additions]
|
|
91
|
+
@base_deletions += counts[:deletions]
|
|
92
|
+
return true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
changes = self.class.changed_lines_by_file(diff)
|
|
96
|
+
return false if changes.empty?
|
|
97
|
+
|
|
98
|
+
changes.each { |path, lines| apply_file_change(path, lines) }
|
|
84
99
|
true
|
|
85
100
|
end
|
|
86
101
|
|
|
@@ -113,12 +128,94 @@ module Kward
|
|
|
113
128
|
previous.last
|
|
114
129
|
end
|
|
115
130
|
|
|
131
|
+
def self.changed_lines_by_file(diff)
|
|
132
|
+
current_path = nil
|
|
133
|
+
changes = Hash.new { |file_changes, path| file_changes[path] = { removed: [], added: [] } }
|
|
134
|
+
removed = []
|
|
135
|
+
added = []
|
|
136
|
+
flush = lambda do
|
|
137
|
+
unmatched = unmatched_lines(removed, added)
|
|
138
|
+
changes[current_path][:removed].concat(unmatched[:removed]) if current_path
|
|
139
|
+
changes[current_path][:added].concat(unmatched[:added]) if current_path
|
|
140
|
+
removed.clear
|
|
141
|
+
added.clear
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
diff.to_s.each_line do |line|
|
|
145
|
+
if line.start_with?("--- ")
|
|
146
|
+
flush.call
|
|
147
|
+
current_path = line[4..].to_s.chomp
|
|
148
|
+
elsif line.start_with?("+") && !line.start_with?("+++")
|
|
149
|
+
added << line[1..]
|
|
150
|
+
elsif line.start_with?("-") && !line.start_with?("---")
|
|
151
|
+
removed << line[1..]
|
|
152
|
+
else
|
|
153
|
+
flush.call
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
flush.call
|
|
157
|
+
changes.reject { |_path, lines| lines[:removed].empty? && lines[:added].empty? }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def self.unmatched_lines(left, right)
|
|
161
|
+
matches = common_line_indexes(left, right)
|
|
162
|
+
left_matches = matches.map(&:first)
|
|
163
|
+
right_matches = matches.map(&:last)
|
|
164
|
+
{
|
|
165
|
+
removed: left.each_index.reject { |index| left_matches.include?(index) }.map { |index| left[index] },
|
|
166
|
+
added: right.each_index.reject { |index| right_matches.include?(index) }.map { |index| right[index] }
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def self.common_line_indexes(left, right)
|
|
171
|
+
lengths = Array.new(left.length + 1) { Array.new(right.length + 1, 0) }
|
|
172
|
+
left.each_with_index do |left_line, left_index|
|
|
173
|
+
right.each_with_index do |right_line, right_index|
|
|
174
|
+
lengths[left_index + 1][right_index + 1] = if left_line == right_line
|
|
175
|
+
lengths[left_index][right_index] + 1
|
|
176
|
+
else
|
|
177
|
+
[lengths[left_index + 1][right_index], lengths[left_index][right_index + 1]].max
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
indexes = []
|
|
183
|
+
left_index = left.length
|
|
184
|
+
right_index = right.length
|
|
185
|
+
while left_index.positive? && right_index.positive?
|
|
186
|
+
if left[left_index - 1] == right[right_index - 1]
|
|
187
|
+
indexes.unshift([left_index - 1, right_index - 1])
|
|
188
|
+
left_index -= 1
|
|
189
|
+
right_index -= 1
|
|
190
|
+
elsif lengths[left_index - 1][right_index] >= lengths[left_index][right_index - 1]
|
|
191
|
+
left_index -= 1
|
|
192
|
+
else
|
|
193
|
+
right_index -= 1
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
indexes
|
|
197
|
+
end
|
|
198
|
+
|
|
116
199
|
def self.parse_record(line)
|
|
117
200
|
JSON.parse(line)
|
|
118
201
|
rescue JSON::ParserError
|
|
119
202
|
nil
|
|
120
203
|
end
|
|
121
204
|
|
|
205
|
+
def apply_file_change(path, lines)
|
|
206
|
+
remove_reverted_lines(@file_changes[path][:added], lines[:removed])
|
|
207
|
+
remove_reverted_lines(@file_changes[path][:removed], lines[:added])
|
|
208
|
+
@file_changes[path][:removed].concat(lines[:removed])
|
|
209
|
+
@file_changes[path][:added].concat(lines[:added])
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def remove_reverted_lines(previous_lines, current_lines)
|
|
213
|
+
current_lines.delete_if do |line|
|
|
214
|
+
index = previous_lines.index(line)
|
|
215
|
+
previous_lines.delete_at(index) if index
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
|
|
122
219
|
def extract_unified_diff(text)
|
|
123
220
|
index = text.index(/^--- /)
|
|
124
221
|
index ? text[index..] : nil
|
data/lib/kward/session_store.rb
CHANGED
|
@@ -592,7 +592,11 @@ module Kward
|
|
|
592
592
|
next unless node
|
|
593
593
|
|
|
594
594
|
parent = nodes[entry["parentId"].to_s]
|
|
595
|
-
parent
|
|
595
|
+
if parent && !parent.equal?(node)
|
|
596
|
+
parent["children"] << node unless parent["children"].include?(node)
|
|
597
|
+
else
|
|
598
|
+
roots << node unless roots.include?(node)
|
|
599
|
+
end
|
|
596
600
|
end
|
|
597
601
|
roots
|
|
598
602
|
end
|
|
@@ -19,7 +19,12 @@ module Kward
|
|
|
19
19
|
multiple_roots = visible_roots.length > 1
|
|
20
20
|
result = []
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
stack = visible_roots.sort_by { |root| session_tree_contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index.map do |root, index|
|
|
23
|
+
[root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots]
|
|
24
|
+
end.reverse
|
|
25
|
+
|
|
26
|
+
until stack.empty?
|
|
27
|
+
node, indent, just_branched, show_connector, is_last, gutters, virtual_root_child = stack.pop
|
|
23
28
|
entry = node[:source]["entry"] || {}
|
|
24
29
|
display_indent = multiple_roots ? [indent - 1, 0].max : indent
|
|
25
30
|
prefix = session_tree_visual_prefix(display_indent, gutters, show_connector && !virtual_root_child, is_last, !node[:children].empty?)
|
|
@@ -40,25 +45,40 @@ module Kward
|
|
|
40
45
|
connector_position = [display_indent - 1, 0].max
|
|
41
46
|
child_gutters = show_connector && !virtual_root_child ? gutters + [{ position: connector_position, show: !is_last }] : gutters
|
|
42
47
|
|
|
43
|
-
children.each_with_index do |child, index|
|
|
44
|
-
|
|
48
|
+
children.each_with_index.reverse_each do |child, index|
|
|
49
|
+
stack << [child, child_indent, multiple_children, multiple_children, index == children.length - 1, child_gutters, false]
|
|
45
50
|
end
|
|
46
51
|
end
|
|
47
52
|
|
|
48
|
-
visible_roots.sort_by { |root| session_tree_contains_active_path?(root, active_path) ? 0 : 1 }.each_with_index do |root, index|
|
|
49
|
-
walk.call(root, multiple_roots ? 1 : 0, multiple_roots, multiple_roots, index == visible_roots.length - 1, [], multiple_roots)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
53
|
result
|
|
53
54
|
end
|
|
54
55
|
|
|
55
56
|
private
|
|
56
57
|
|
|
57
58
|
def visible_session_tree_nodes(node)
|
|
58
|
-
|
|
59
|
-
|
|
59
|
+
results = {}
|
|
60
|
+
stack = [[node, false, {}]]
|
|
61
|
+
|
|
62
|
+
until stack.empty?
|
|
63
|
+
current, visited, seen = stack.pop
|
|
64
|
+
node_key = current.object_id
|
|
65
|
+
next if seen[node_key]
|
|
66
|
+
|
|
67
|
+
if visited
|
|
68
|
+
children = Array(current["children"]).flat_map { |child| results[child.object_id] || [] }
|
|
69
|
+
results[node_key] = if hidden_session_tree_entry?(current["entry"] || {})
|
|
70
|
+
children
|
|
71
|
+
else
|
|
72
|
+
[{ source: current, children: children }]
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
branch_seen = seen.merge(node_key => true)
|
|
76
|
+
stack << [current, true, seen]
|
|
77
|
+
Array(current["children"]).reverse_each { |child| stack << [child, false, branch_seen] unless branch_seen[child.object_id] }
|
|
78
|
+
end
|
|
79
|
+
end
|
|
60
80
|
|
|
61
|
-
[
|
|
81
|
+
results[node.object_id] || []
|
|
62
82
|
end
|
|
63
83
|
|
|
64
84
|
def hidden_session_tree_entry?(entry)
|
|
@@ -110,7 +130,8 @@ module Kward
|
|
|
110
130
|
return "" if display_indent.to_i <= 0
|
|
111
131
|
|
|
112
132
|
connector_position = show_connector ? display_indent - 1 : -1
|
|
113
|
-
|
|
133
|
+
indentation = " "
|
|
134
|
+
indentation + (0...(display_indent * 3)).map do |index|
|
|
114
135
|
level = index / 3
|
|
115
136
|
position = index % 3
|
|
116
137
|
gutter = gutters.find { |candidate| candidate[:position] == level }
|
|
@@ -132,15 +153,28 @@ module Kward
|
|
|
132
153
|
end
|
|
133
154
|
|
|
134
155
|
def session_tree_contains_active_path?(node, active_path)
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
stack = [node]
|
|
157
|
+
seen = {}
|
|
158
|
+
until stack.empty?
|
|
159
|
+
current = stack.pop
|
|
160
|
+
next if seen[current.object_id]
|
|
161
|
+
|
|
162
|
+
seen[current.object_id] = true
|
|
163
|
+
entry_id = (current[:source]["entry"] || {})["id"].to_s
|
|
164
|
+
return true if active_path.include?(entry_id)
|
|
165
|
+
|
|
166
|
+
stack.concat(current[:children])
|
|
167
|
+
end
|
|
168
|
+
false
|
|
137
169
|
end
|
|
138
170
|
|
|
139
171
|
def session_tree_active_path(roots, leaf_id)
|
|
140
172
|
by_id = session_tree_entries_by_id(roots)
|
|
141
173
|
ids = []
|
|
142
174
|
entry = by_id[leaf_id.to_s]
|
|
143
|
-
|
|
175
|
+
seen = {}
|
|
176
|
+
while entry && !seen[entry["id"].to_s]
|
|
177
|
+
seen[entry["id"].to_s] = true
|
|
144
178
|
ids << entry["id"].to_s
|
|
145
179
|
entry = by_id[entry["parentId"].to_s]
|
|
146
180
|
end
|
|
@@ -150,8 +184,12 @@ module Kward
|
|
|
150
184
|
def session_tree_entries_by_id(roots)
|
|
151
185
|
roots.each_with_object({}) do |root, map|
|
|
152
186
|
stack = [root]
|
|
187
|
+
seen = {}
|
|
153
188
|
until stack.empty?
|
|
154
189
|
node = stack.pop
|
|
190
|
+
next if seen[node.object_id]
|
|
191
|
+
|
|
192
|
+
seen[node.object_id] = true
|
|
155
193
|
entry = node["entry"] || {}
|
|
156
194
|
map[entry["id"].to_s] = entry unless entry["id"].to_s.empty?
|
|
157
195
|
stack.concat(Array(node["children"]))
|
|
@@ -162,8 +200,12 @@ module Kward
|
|
|
162
200
|
def session_tree_tool_calls(roots)
|
|
163
201
|
roots.each_with_object({}) do |root, tool_calls|
|
|
164
202
|
stack = [root]
|
|
203
|
+
seen = {}
|
|
165
204
|
until stack.empty?
|
|
166
205
|
node = stack.pop
|
|
206
|
+
next if seen[node.object_id]
|
|
207
|
+
|
|
208
|
+
seen[node.object_id] = true
|
|
167
209
|
entry = node["entry"] || {}
|
|
168
210
|
message = entry["message"]
|
|
169
211
|
if entry["type"] == "message" && message.is_a?(Hash) && message_role(message) == "assistant"
|
data/lib/kward/version.rb
CHANGED