kward 0.69.1 → 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 +22 -0
- 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/session_diff.rb +106 -9
- data/lib/kward/session_tree_renderer.rb +2 -1
- 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)
|
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
|
|
@@ -130,7 +130,8 @@ module Kward
|
|
|
130
130
|
return "" if display_indent.to_i <= 0
|
|
131
131
|
|
|
132
132
|
connector_position = show_connector ? display_indent - 1 : -1
|
|
133
|
-
|
|
133
|
+
indentation = " "
|
|
134
|
+
indentation + (0...(display_indent * 3)).map do |index|
|
|
134
135
|
level = index / 3
|
|
135
136
|
position = index % 3
|
|
136
137
|
gutter = gutters.find { |candidate| candidate[:position] == level }
|
data/lib/kward/version.rb
CHANGED