kward 0.69.1 → 0.71.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/.github/workflows/pages.yml +1 -1
- data/CHANGELOG.md +68 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +90 -2
- data/README.md +30 -6
- data/Rakefile +96 -0
- data/doc/agent-tools.md +43 -0
- data/doc/api.md +92 -0
- data/doc/authentication.md +39 -25
- data/doc/configuration.md +2 -16
- data/doc/context-tools.md +70 -0
- data/doc/getting-started.md +3 -1
- data/doc/plugins.md +2 -2
- data/doc/releasing.md +14 -5
- data/doc/rpc.md +3 -11
- data/doc/session-management.md +220 -0
- data/doc/usage.md +13 -7
- data/doc/workspace-tools.md +105 -0
- data/lib/kward/cli/commands.rb +8 -0
- data/lib/kward/cli/openrouter_commands.rb +55 -0
- data/lib/kward/cli/prompt_interface.rb +85 -7
- data/lib/kward/cli/rendering.rb +11 -6
- data/lib/kward/cli/sessions.rb +454 -15
- data/lib/kward/cli/settings.rb +0 -30
- data/lib/kward/cli/slash_commands.rb +38 -11
- data/lib/kward/cli.rb +14 -0
- data/lib/kward/compactor.rb +4 -1
- data/lib/kward/config_files.rb +4 -6
- data/lib/kward/conversation.rb +49 -5
- data/lib/kward/model/client.rb +37 -50
- data/lib/kward/model/context_usage.rb +13 -6
- data/lib/kward/model/model_info.rb +92 -9
- data/lib/kward/model/payloads.rb +2 -0
- data/lib/kward/openrouter_model_cache.rb +120 -0
- data/lib/kward/plugin_registry.rb +47 -1
- data/lib/kward/prompt_interface/banner.rb +16 -51
- data/lib/kward/prompt_interface/composer_controller.rb +60 -87
- data/lib/kward/prompt_interface/composer_renderer.rb +7 -1
- data/lib/kward/prompt_interface/key_handler.rb +31 -10
- data/lib/kward/prompt_interface/layout.rb +2 -2
- data/lib/kward/prompt_interface/overlay_renderer.rb +24 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +23 -2
- data/lib/kward/prompt_interface/question_prompt.rb +34 -42
- data/lib/kward/prompt_interface/runtime_state.rb +6 -1
- data/lib/kward/prompt_interface/screen.rb +10 -4
- data/lib/kward/prompt_interface/selection_prompt.rb +518 -61
- data/lib/kward/prompt_interface/slash_overlay.rb +4 -4
- data/lib/kward/prompt_interface/transcript_buffer.rb +7 -16
- data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
- data/lib/kward/prompt_interface.rb +31 -32
- data/lib/kward/prompts/commands.rb +6 -3
- data/lib/kward/prompts.rb +2 -2
- data/lib/kward/rpc/server.rb +3 -8
- data/lib/kward/rpc/session_manager.rb +19 -8
- data/lib/kward/session_diff.rb +106 -9
- data/lib/kward/session_store.rb +23 -4
- data/lib/kward/session_tree_renderer.rb +2 -1
- data/lib/kward/telemetry/logger.rb +5 -3
- data/lib/kward/tool_output_compactor.rb +127 -0
- data/lib/kward/tools/base.rb +8 -2
- data/lib/kward/tools/registry.rb +37 -6
- data/lib/kward/tools/retrieve_tool_output.rb +71 -0
- data/lib/kward/tools/search/web.rb +2 -2
- data/lib/kward/tools/summarize_file_structure.rb +29 -0
- data/lib/kward/tools/tool_call.rb +2 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace.rb +58 -2
- data/templates/default/fulldoc/html/css/kward.css +570 -78
- data/templates/default/fulldoc/html/full_list.erb +107 -0
- data/templates/default/fulldoc/html/js/kward.js +259 -97
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/kward_navigation.rb +91 -0
- data/templates/default/layout/html/layout.erb +59 -13
- data/templates/default/layout/html/setup.rb +34 -39
- metadata +13 -3
- data/lib/kward/resources/avatar_kward_logo.rb +0 -50
- data/lib/kward/resources/pixel_logo.rb +0 -232
|
@@ -58,14 +58,14 @@ module Kward
|
|
|
58
58
|
matches = slash_overlay_matches
|
|
59
59
|
return if matches.empty?
|
|
60
60
|
|
|
61
|
-
@slash_selection_index = (@slash_selection_index
|
|
61
|
+
@slash_selection_index = previous_list_selection_index(@slash_selection_index, matches.length)
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
def select_next_slash_command
|
|
65
65
|
matches = slash_overlay_matches
|
|
66
66
|
return if matches.empty?
|
|
67
67
|
|
|
68
|
-
@slash_selection_index = (@slash_selection_index
|
|
68
|
+
@slash_selection_index = next_list_selection_index(@slash_selection_index, matches.length)
|
|
69
69
|
end
|
|
70
70
|
|
|
71
71
|
def complete_selected_slash_command
|
|
@@ -92,8 +92,8 @@ module Kward
|
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
def visible_slash_overlay_matches(matches, height: screen_height)
|
|
95
|
-
max_rows =
|
|
96
|
-
start =
|
|
95
|
+
max_rows = max_overlay_list_rows(height)
|
|
96
|
+
start = centered_list_window_start(@slash_selection_index, matches.length, max_rows)
|
|
97
97
|
{ start: start, commands: matches[start, max_rows] || [] }
|
|
98
98
|
end
|
|
99
99
|
|
|
@@ -10,7 +10,6 @@ module Kward
|
|
|
10
10
|
@limit = limit
|
|
11
11
|
@text = +""
|
|
12
12
|
@display_rows_cache_width = nil
|
|
13
|
-
@display_rows_cache_banner_count = nil
|
|
14
13
|
@display_rows_cache = nil
|
|
15
14
|
end
|
|
16
15
|
|
|
@@ -42,30 +41,23 @@ module Kward
|
|
|
42
41
|
@text
|
|
43
42
|
end
|
|
44
43
|
|
|
45
|
-
def viewport_text(row_count, width
|
|
46
|
-
viewport_rows(row_count, width
|
|
44
|
+
def viewport_text(row_count, width)
|
|
45
|
+
viewport_rows(row_count, width).join("\n")
|
|
47
46
|
end
|
|
48
47
|
|
|
49
|
-
def viewport_rows(row_count, width
|
|
48
|
+
def viewport_rows(row_count, width)
|
|
50
49
|
return [] unless row_count.positive?
|
|
51
50
|
|
|
52
|
-
rows = display_rows(width
|
|
51
|
+
rows = display_rows(width).last(row_count)
|
|
53
52
|
rows = ([""] * (row_count - rows.length)) + rows if rows.length < row_count
|
|
54
53
|
rows
|
|
55
54
|
end
|
|
56
55
|
|
|
57
|
-
def display_rows(width
|
|
58
|
-
if @display_rows_cache_width == width && @
|
|
59
|
-
return @display_rows_cache
|
|
60
|
-
end
|
|
56
|
+
def display_rows(width)
|
|
57
|
+
return @display_rows_cache if @display_rows_cache_width == width && @display_rows_cache
|
|
61
58
|
|
|
62
|
-
rows = []
|
|
63
|
-
visual_banner_count.times { rows.concat(banner_rows.call(width)) }
|
|
64
|
-
rows << "" if visual_banner_count.positive? && @text.empty?
|
|
65
|
-
rows.concat(text_display_rows(width))
|
|
66
59
|
@display_rows_cache_width = width
|
|
67
|
-
@
|
|
68
|
-
@display_rows_cache = rows
|
|
60
|
+
@display_rows_cache = text_display_rows(width)
|
|
69
61
|
end
|
|
70
62
|
|
|
71
63
|
def text_display_rows(width)
|
|
@@ -77,7 +69,6 @@ module Kward
|
|
|
77
69
|
|
|
78
70
|
def invalidate_display_rows_cache
|
|
79
71
|
@display_rows_cache_width = nil
|
|
80
|
-
@display_rows_cache_banner_count = nil
|
|
81
72
|
@display_rows_cache = nil
|
|
82
73
|
end
|
|
83
74
|
end
|
|
@@ -11,7 +11,7 @@ module Kward
|
|
|
11
11
|
prepare_transcript_output_locked unless @restoring_transcript
|
|
12
12
|
if label && @stream_state.block != label
|
|
13
13
|
ensure_transcript_block_separator_locked
|
|
14
|
-
write_transcript_text_locked("#{colored("#{transcript_label(label)}>", *label_styles(label))}
|
|
14
|
+
write_transcript_text_locked("#{colored("#{transcript_label(label)}>", *label_styles(label))} ")
|
|
15
15
|
@stream_state.start_block(label)
|
|
16
16
|
end
|
|
17
17
|
write_transcript_text_locked(delta) unless delta.empty?
|
|
@@ -82,11 +82,11 @@ module Kward
|
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def transcript_renderable?
|
|
85
|
-
|
|
85
|
+
!@transcript_buffer.empty?
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def transcript_display_rows(width)
|
|
89
|
-
@transcript_buffer.display_rows(width
|
|
89
|
+
@transcript_buffer.display_rows(width)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def transcript_text_display_rows(width)
|
|
@@ -41,7 +41,6 @@ module Kward
|
|
|
41
41
|
FOOTER_REFRESH_INTERVAL = 1.0
|
|
42
42
|
COMPOSER_MAX_INPUT_ROWS = 6
|
|
43
43
|
TRANSCRIPT_BUFFER_LIMIT = 200_000
|
|
44
|
-
BANNER_LOGO_PIXELS = Banner::LOGO_PIXELS
|
|
45
44
|
BANNER_MESSAGE = Banner::MESSAGE
|
|
46
45
|
|
|
47
46
|
include SlashOverlay
|
|
@@ -70,6 +69,8 @@ module Kward
|
|
|
70
69
|
EXIT_INPUT = :exit_input
|
|
71
70
|
CANCEL_INPUT = :cancel_input
|
|
72
71
|
SELECT_CANCEL = :select_cancel
|
|
72
|
+
SELECT_CONTINUE = :select_continue
|
|
73
|
+
SELECT_ACTION_MINIMUM_BUSY_SECONDS = 1.0
|
|
73
74
|
|
|
74
75
|
# Submitted input string carrying optional display text for transcripts.
|
|
75
76
|
class SubmittedInput < String
|
|
@@ -81,7 +82,7 @@ module Kward
|
|
|
81
82
|
end
|
|
82
83
|
end
|
|
83
84
|
|
|
84
|
-
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil,
|
|
85
|
+
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil)
|
|
85
86
|
@input_io = input
|
|
86
87
|
@output_io = output
|
|
87
88
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
@@ -105,7 +106,6 @@ module Kward
|
|
|
105
106
|
@last_composer_rows = []
|
|
106
107
|
@cursor_rendered_row = 0
|
|
107
108
|
@transcript_buffer = TranscriptBuffer.new(limit: TRANSCRIPT_BUFFER_LIMIT)
|
|
108
|
-
@visual_banner_count = 0
|
|
109
109
|
@transcript_viewport_rows = 0
|
|
110
110
|
@restoring_transcript = false
|
|
111
111
|
@pending_keys = []
|
|
@@ -128,10 +128,10 @@ module Kward
|
|
|
128
128
|
@busy_help = busy_help
|
|
129
129
|
@attachment_badges = attachment_badges
|
|
130
130
|
@attachment_parser = attachment_parser
|
|
131
|
-
@banner = Banner.new(message: banner_message,
|
|
131
|
+
@banner = Banner.new(message: banner_message, 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,11 +198,11 @@ 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
|
-
@visual_banner_count = 0
|
|
206
206
|
@transcript_viewport_rows = 0
|
|
207
207
|
@stream_state.finish_block
|
|
208
208
|
@stream_state.reset
|
|
@@ -213,9 +213,9 @@ module Kward
|
|
|
213
213
|
ensure
|
|
214
214
|
@mutex.synchronize do
|
|
215
215
|
@restoring_transcript = false
|
|
216
|
-
@output_io.print(SYNCHRONIZED_OUTPUT_DISABLE)
|
|
217
216
|
width, height = screen_size
|
|
218
217
|
redraw_screen_locked(width: width, height: height)
|
|
218
|
+
@output_io.print(SYNCHRONIZED_OUTPUT_DISABLE)
|
|
219
219
|
@output_io.flush
|
|
220
220
|
end
|
|
221
221
|
end
|
|
@@ -270,23 +270,19 @@ module Kward
|
|
|
270
270
|
answer.start_with?("y")
|
|
271
271
|
end
|
|
272
272
|
|
|
273
|
-
def
|
|
273
|
+
def picker_choice_width
|
|
274
|
+
[overlay_card_width(screen_width) - 6, 1].max
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def select(message, choices, title: "Sessions", custom: false, initial_index: 0, action_keys: {}, action_handlers: {})
|
|
274
278
|
return nil if choices.empty? && !custom
|
|
275
279
|
|
|
276
280
|
start
|
|
277
281
|
@mutex.synchronize do
|
|
278
|
-
|
|
279
|
-
self.composer_input = ""
|
|
280
|
-
self.composer_cursor = 0
|
|
281
|
-
@composer.clear_attachments
|
|
282
|
-
@pending_keys.clear
|
|
283
|
-
@asking = true
|
|
284
|
-
@busy = false
|
|
285
|
-
@queued_count = 0
|
|
282
|
+
prepare_modal_input_locked(message, clear_attachments: true)
|
|
286
283
|
choice_labels = choices.map(&:to_s)
|
|
287
284
|
selection_index = choice_labels.empty? ? 0 : [[initial_index.to_i, 0].max, choice_labels.length - 1].min
|
|
288
|
-
@select_state = { choices: choice_labels, selection_index: selection_index, title: title.to_s, custom: custom }
|
|
289
|
-
reset_history_navigation
|
|
285
|
+
@select_state = { choices: choice_labels, selection_index: selection_index, title: title.to_s, custom: custom, action_keys: normalized_select_action_keys(action_keys), search_active: false }
|
|
290
286
|
render_prompt_locked
|
|
291
287
|
end
|
|
292
288
|
|
|
@@ -300,12 +296,20 @@ module Kward
|
|
|
300
296
|
render_prompt_locked if resized || footer_refreshed
|
|
301
297
|
else
|
|
302
298
|
result = handle_select_key(key)
|
|
303
|
-
|
|
299
|
+
result = drain_pending_select_keys_locked(result)
|
|
300
|
+
render_prompt_locked unless result.is_a?(String) || select_action_result?(result) || result == SELECT_CANCEL
|
|
304
301
|
end
|
|
305
302
|
end
|
|
306
303
|
|
|
307
|
-
if
|
|
308
|
-
|
|
304
|
+
if select_action_result?(result) && select_action_handler(result, action_handlers)
|
|
305
|
+
action_result = run_select_action(result, select_action_handler(result, action_handlers))
|
|
306
|
+
next if action_result == SELECT_CONTINUE
|
|
307
|
+
|
|
308
|
+
return action_result
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
if result.is_a?(String) || select_action_result?(result) || result == SELECT_CANCEL
|
|
312
|
+
finish_select_prompt(render: !select_deferred_finish_render?(result))
|
|
309
313
|
return result == SELECT_CANCEL ? nil : result
|
|
310
314
|
end
|
|
311
315
|
|
|
@@ -425,20 +429,16 @@ module Kward
|
|
|
425
429
|
end
|
|
426
430
|
end
|
|
427
431
|
|
|
428
|
-
def print_visual_banner
|
|
432
|
+
def print_visual_banner(message = nil)
|
|
429
433
|
@mutex.synchronize do
|
|
430
434
|
width, height = screen_size
|
|
431
|
-
rows = banner_rows(width)
|
|
435
|
+
rows = banner_rows(width, message: message)
|
|
432
436
|
return if rows.empty?
|
|
433
437
|
|
|
434
438
|
with_synchronized_output_locked do
|
|
435
439
|
prepare_transcript_output_locked
|
|
436
|
-
rows.
|
|
437
|
-
|
|
438
|
-
write_visual_transcript_text_locked("\n")
|
|
439
|
-
end
|
|
440
|
-
@visual_banner_count += 1
|
|
441
|
-
invalidate_transcript_display_rows_cache
|
|
440
|
+
write_transcript_text_locked(rows.join("\n"))
|
|
441
|
+
write_transcript_text_locked("\n")
|
|
442
442
|
remember_transcript_viewport_locked(height)
|
|
443
443
|
@stream_state.finish_block
|
|
444
444
|
restore_composer_cursor_locked
|
|
@@ -482,7 +482,6 @@ module Kward
|
|
|
482
482
|
def clear_transcript
|
|
483
483
|
@mutex.synchronize do
|
|
484
484
|
@transcript_buffer.clear
|
|
485
|
-
@visual_banner_count = 0
|
|
486
485
|
@transcript_viewport_rows = 0
|
|
487
486
|
@stream_state.finish_block
|
|
488
487
|
@stream_state.reset
|
|
@@ -8,10 +8,14 @@ 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]" },
|
|
14
|
+
{ name: "rename", description: "Rename the current session.", argument_hint: "<name>" },
|
|
13
15
|
{ name: "clone", description: "Clone the current session.", argument_hint: "" },
|
|
14
|
-
{ name: "
|
|
16
|
+
{ name: "fork", description: "Fork from an earlier prompt into a new session.", argument_hint: "" },
|
|
17
|
+
{ name: "rewind", description: "Revisit an earlier prompt and try a different direction.", argument_hint: "" },
|
|
18
|
+
{ name: "tree", description: "Inspect and navigate the full technical session tree.", argument_hint: "" },
|
|
15
19
|
{ name: "copy", description: "Copy clean session text to the clipboard.", argument_hint: "[last|transcript]" },
|
|
16
20
|
{ name: "export", description: "Export the current session as Markdown.", argument_hint: "[path]" },
|
|
17
21
|
{ name: "compact", description: "Compact the current conversation context.", argument_hint: "[instructions]" },
|
|
@@ -19,7 +23,6 @@ module Kward
|
|
|
19
23
|
{ name: "settings", description: "Configure prompt overlays.", argument_hint: "" },
|
|
20
24
|
{ name: "login", description: "Log in with an OAuth provider.", argument_hint: "" },
|
|
21
25
|
{ name: "model", description: "Select the default model.", argument_hint: "" },
|
|
22
|
-
{ name: "openrouter/catalog", description: "List the full OpenRouter model catalog.", argument_hint: "" },
|
|
23
26
|
{ name: "reasoning", description: "Select reasoning effort.", argument_hint: "" },
|
|
24
27
|
{ name: "reload", description: "Reload installed plugins.", argument_hint: "" },
|
|
25
28
|
{ name: "status", description: "Show the current status message.", argument_hint: "" },
|
data/lib/kward/prompts.rb
CHANGED
|
@@ -32,9 +32,9 @@ module Kward
|
|
|
32
32
|
|
|
33
33
|
def base_prompt
|
|
34
34
|
<<~PROMPT.strip
|
|
35
|
-
You are Kward, a concise practical CLI coding agent.
|
|
35
|
+
You are Kward, a concise practical CLI coding agent. Use tools to understand and modify software projects. Inspect files before changing them, make the smallest correct change, preserve existing style, and summarize what changed. Be honest about limitations.
|
|
36
36
|
|
|
37
|
-
For web research, use web_search to discover sources,
|
|
37
|
+
For web research, use web_search to discover sources, fetch_content for important human-readable pages, and fetch_raw for machine-readable resources such as JSON, YAML, XML, RSS, OpenAPI specs, and plain text. Prefer official or primary sources and cite or mention the URLs you relied on.
|
|
38
38
|
PROMPT
|
|
39
39
|
end
|
|
40
40
|
|
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -49,7 +49,7 @@ module Kward
|
|
|
49
49
|
"sessions/tree", "sessions/tree/setLabel", "sessions/tree/navigate",
|
|
50
50
|
"sessions/export", "sessions/delete", "sessions/close", "sessions/transcript"
|
|
51
51
|
].freeze
|
|
52
|
-
MODEL_METHODS = ["models/list", "models/current", "models/set", "reasoning/set"
|
|
52
|
+
MODEL_METHODS = ["models/list", "models/current", "models/set", "reasoning/set"].freeze
|
|
53
53
|
AUTH_METHODS = [
|
|
54
54
|
"auth/status", "auth/providers", "auth/loginWithApiKey", "auth/logoutProvider",
|
|
55
55
|
"auth/loginWithOAuth", "auth/startOpenAILogin", "auth/submitOpenAICode", "auth/loginStatus"
|
|
@@ -166,8 +166,6 @@ module Kward
|
|
|
166
166
|
prompts_expand(params)
|
|
167
167
|
when "models/list"
|
|
168
168
|
models_list
|
|
169
|
-
when "openrouter/catalog"
|
|
170
|
-
openrouter_catalog
|
|
171
169
|
when "models/current"
|
|
172
170
|
models_current
|
|
173
171
|
when "models/set"
|
|
@@ -362,7 +360,8 @@ module Kward
|
|
|
362
360
|
supported: true,
|
|
363
361
|
methods: MODEL_METHODS,
|
|
364
362
|
fields: ["provider", "id", "name", "reasoning", "reasoningEffort", "contextWindow"],
|
|
365
|
-
scopedModels: false
|
|
363
|
+
scopedModels: false,
|
|
364
|
+
openRouterRefresh: { supported: false, reason: "cliOnlyCacheRefresh" }
|
|
366
365
|
},
|
|
367
366
|
runtime: {
|
|
368
367
|
supported: true,
|
|
@@ -458,10 +457,6 @@ module Kward
|
|
|
458
457
|
{ models: @session_manager.available_models }
|
|
459
458
|
end
|
|
460
459
|
|
|
461
|
-
def openrouter_catalog
|
|
462
|
-
{ models: @session_manager.openrouter_catalog }
|
|
463
|
-
end
|
|
464
|
-
|
|
465
460
|
def config_update(params)
|
|
466
461
|
config = @config_manager.update(params.fetch("values"))
|
|
467
462
|
@session_manager.refresh_client_config
|
|
@@ -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)
|
|
@@ -485,11 +485,6 @@ module Kward
|
|
|
485
485
|
normalized
|
|
486
486
|
end
|
|
487
487
|
|
|
488
|
-
def openrouter_catalog
|
|
489
|
-
models = @client.respond_to?(:openrouter_catalog) ? Array(@client.openrouter_catalog) : []
|
|
490
|
-
models.map { |model| normalize_model(model) }
|
|
491
|
-
end
|
|
492
|
-
|
|
493
488
|
def current_model
|
|
494
489
|
provider = @client.respond_to?(:current_provider) ? @client.current_provider : nil
|
|
495
490
|
model = @client.respond_to?(:current_model) ? @client.current_model : nil
|
|
@@ -503,7 +498,7 @@ module Kward
|
|
|
503
498
|
model = rpc_session.conversation.model || current[:id]
|
|
504
499
|
reasoning_effort = rpc_session.conversation.reasoning_effort || current_reasoning_effort
|
|
505
500
|
reasoning_effort = nil unless ModelInfo.reasoning_supported?(provider, model)
|
|
506
|
-
context_window =
|
|
501
|
+
context_window = context_window_for(provider, model)
|
|
507
502
|
normalize_model(
|
|
508
503
|
provider: provider,
|
|
509
504
|
id: model,
|
|
@@ -666,6 +661,11 @@ module Kward
|
|
|
666
661
|
end
|
|
667
662
|
|
|
668
663
|
def normalize_model(model)
|
|
664
|
+
unless model.key?(:contextWindow) || model.key?("contextWindow")
|
|
665
|
+
provider = model[:provider] || model["provider"]
|
|
666
|
+
id = model[:id] || model["id"] || model[:model] || model["model"]
|
|
667
|
+
model = model.merge(contextWindow: context_window_for(provider, id))
|
|
668
|
+
end
|
|
669
669
|
ModelInfo.normalize(
|
|
670
670
|
model,
|
|
671
671
|
current_provider: (@client.current_provider if @client.respond_to?(:current_provider)),
|
|
@@ -674,6 +674,17 @@ module Kward
|
|
|
674
674
|
)
|
|
675
675
|
end
|
|
676
676
|
|
|
677
|
+
def context_window_for(provider, model)
|
|
678
|
+
provider = ModelInfo.provider_label(provider)
|
|
679
|
+
return @client.context_window(provider, model) if @client.respond_to?(:context_window) && @client.method(:context_window).arity != 0
|
|
680
|
+
|
|
681
|
+
if @client.respond_to?(:current_context_window) && @client.respond_to?(:current_provider) && @client.respond_to?(:current_model)
|
|
682
|
+
return @client.current_context_window if provider == @client.current_provider && model == @client.current_model
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
ModelInfo.context_window(provider, model)
|
|
686
|
+
end
|
|
687
|
+
|
|
677
688
|
def active_persona_label(rpc_session)
|
|
678
689
|
ConfigFiles.active_persona_label(
|
|
679
690
|
workspace_root: rpc_session.workspace_root,
|
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
|
@@ -496,9 +496,29 @@ module Kward
|
|
|
496
496
|
|
|
497
497
|
def session_header(records, path)
|
|
498
498
|
header = records.find { |record| record["type"] == "session" }
|
|
499
|
-
|
|
499
|
+
return header if header && header["id"].to_s != ""
|
|
500
500
|
|
|
501
|
-
|
|
501
|
+
recovered = recovered_session_header(records, path)
|
|
502
|
+
return recovered if recovered
|
|
503
|
+
|
|
504
|
+
raise "Invalid Kward session file: #{path}"
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def recovered_session_header(records, path)
|
|
508
|
+
return nil unless records.any? { |record| ["message", "session_info", "system_prompt", "memory_state"].include?(record["type"]) }
|
|
509
|
+
|
|
510
|
+
basename = File.basename(path)
|
|
511
|
+
match = basename.match(/\A(?<timestamp>\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z)_(?<id>[0-9a-fA-F-]{36})\.jsonl\z/)
|
|
512
|
+
return nil unless match
|
|
513
|
+
|
|
514
|
+
timestamp = match[:timestamp].tr("-", ":").sub(/\A(\d{4}):(\d{2}):(\d{2})T/, "\\1-\\2-\\3T")
|
|
515
|
+
{
|
|
516
|
+
"type" => "session",
|
|
517
|
+
"version" => VERSION,
|
|
518
|
+
"id" => match[:id],
|
|
519
|
+
"timestamp" => timestamp,
|
|
520
|
+
"cwd" => @cwd
|
|
521
|
+
}
|
|
502
522
|
end
|
|
503
523
|
|
|
504
524
|
def session_named?(session)
|
|
@@ -756,8 +776,7 @@ module Kward
|
|
|
756
776
|
|
|
757
777
|
def session_info(path)
|
|
758
778
|
records = records_from_file(path)
|
|
759
|
-
header = records
|
|
760
|
-
return nil unless header && header["id"].to_s != ""
|
|
779
|
+
header = session_header(records, path)
|
|
761
780
|
|
|
762
781
|
messages = restored_messages(records)
|
|
763
782
|
name = session_name(records)
|
|
@@ -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 }
|
|
@@ -8,13 +8,14 @@ require_relative "../rpc/redactor"
|
|
|
8
8
|
module Kward
|
|
9
9
|
# Append-only JSONL telemetry logger with secret-conscious error payloads.
|
|
10
10
|
class TelemetryLogger
|
|
11
|
-
CATEGORIES = %w[tokens performance tools errors].freeze
|
|
11
|
+
CATEGORIES = %w[tokens performance tools errors compaction].freeze
|
|
12
12
|
ENV_KEYS = {
|
|
13
13
|
"enabled" => "KWARD_LOGGING",
|
|
14
14
|
"tokens" => "KWARD_LOGGING_TOKENS",
|
|
15
15
|
"performance" => "KWARD_LOGGING_PERFORMANCE",
|
|
16
16
|
"tools" => "KWARD_LOGGING_TOOLS",
|
|
17
|
-
"errors" => "KWARD_LOGGING_ERRORS"
|
|
17
|
+
"errors" => "KWARD_LOGGING_ERRORS",
|
|
18
|
+
"compaction" => "KWARD_LOGGING_COMPACTION"
|
|
18
19
|
}.freeze
|
|
19
20
|
DEFAULT_MAX_BYTES = 10 * 1024 * 1024
|
|
20
21
|
|
|
@@ -101,7 +102,8 @@ module Kward
|
|
|
101
102
|
"tokens" => truthy?(logging["tokens"]),
|
|
102
103
|
"performance" => truthy?(logging["performance"]),
|
|
103
104
|
"tools" => truthy?(logging["tools"]),
|
|
104
|
-
"errors" => truthy?(logging["errors"])
|
|
105
|
+
"errors" => truthy?(logging["errors"]),
|
|
106
|
+
"compaction" => truthy?(logging["compaction"])
|
|
105
107
|
}
|
|
106
108
|
rescue StandardError
|
|
107
109
|
CATEGORIES.each_with_object({ "enabled" => false }) { |category, result| result[category] = false }
|