kward 0.70.0 → 0.72.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 +89 -3
- data/Gemfile +2 -0
- data/Gemfile.lock +90 -2
- data/README.md +34 -6
- data/Rakefile +96 -0
- data/doc/agent-tools.md +52 -0
- data/doc/api.md +92 -0
- data/doc/authentication.md +58 -23
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +102 -13
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +83 -0
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +74 -3
- data/doc/releasing.md +45 -8
- data/doc/rpc.md +77 -15
- data/doc/session-management.md +254 -0
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +60 -15
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +144 -0
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +41 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/openrouter_commands.rb +55 -0
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +111 -6
- data/lib/kward/cli/rendering.rb +11 -6
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +262 -13
- data/lib/kward/cli/settings.rb +216 -37
- data/lib/kward/cli/slash_commands.rb +439 -8
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +171 -26
- data/lib/kward/compactor.rb +4 -1
- data/lib/kward/config_files.rb +125 -5
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +59 -22
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- 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 -16
- data/lib/kward/model/payloads.rb +2 -0
- data/lib/kward/openrouter_model_cache.rb +120 -0
- data/lib/kward/plugin_registry.rb +108 -1
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/banner.rb +16 -51
- data/lib/kward/prompt_interface/composer_controller.rb +124 -83
- data/lib/kward/prompt_interface/composer_renderer.rb +116 -14
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +416 -43
- data/lib/kward/prompt_interface/layout.rb +2 -2
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +32 -13
- data/lib/kward/prompt_interface/question_prompt.rb +122 -82
- data/lib/kward/prompt_interface/runtime_state.rb +49 -1
- data/lib/kward/prompt_interface/screen.rb +17 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +511 -58
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +13 -16
- data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
- data/lib/kward/prompt_interface.rb +307 -35
- data/lib/kward/prompts/commands.rb +7 -1
- data/lib/kward/prompts.rb +4 -2
- data/lib/kward/rpc/server.rb +45 -11
- data/lib/kward/rpc/session_manager.rb +52 -53
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +67 -4
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/telemetry/logger.rb +5 -3
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tool_output_compactor.rb +127 -0
- data/lib/kward/tools/base.rb +8 -2
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +92 -15
- 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 +12 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +154 -12
- data/templates/default/fulldoc/html/css/kward.css +362 -42
- data/templates/default/fulldoc/html/full_list.erb +107 -0
- data/templates/default/fulldoc/html/js/kward.js +161 -2
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/kward_navigation.rb +102 -0
- data/templates/default/layout/html/layout.erb +43 -10
- data/templates/default/layout/html/setup.rb +39 -38
- metadata +65 -3
- data/lib/kward/resources/avatar_kward_logo.rb +0 -50
- data/lib/kward/resources/pixel_logo.rb +0 -232
data/lib/kward/workspace.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Kward
|
|
|
20
20
|
MAX_FILE_BYTES = 256 * 1024
|
|
21
21
|
MAX_READ_OUTPUT_BYTES = 50 * 1024
|
|
22
22
|
MAX_READ_OUTPUT_LINES = 2_000
|
|
23
|
-
MAX_COMMAND_OUTPUT_BYTES =
|
|
23
|
+
MAX_COMMAND_OUTPUT_BYTES = 128 * 1024
|
|
24
24
|
MAX_EDIT_DIFF_BYTES = 8 * 1024
|
|
25
25
|
DEFAULT_COMMAND_TIMEOUT_SECONDS = 30
|
|
26
26
|
|
|
@@ -54,7 +54,7 @@ module Kward
|
|
|
54
54
|
# The returned string is user/model-facing and includes continuation notices
|
|
55
55
|
# when output is truncated. Errors are returned as `"Error: ..."` strings so
|
|
56
56
|
# tool calls can be persisted in the conversation without raising.
|
|
57
|
-
def read_file(path, offset: nil, limit: nil)
|
|
57
|
+
def read_file(path, offset: nil, limit: nil, mode: nil, max_bytes: nil)
|
|
58
58
|
resolved = workspace_path(path)
|
|
59
59
|
return "Error: not a file: #{path}" unless File.file?(resolved)
|
|
60
60
|
|
|
@@ -64,7 +64,40 @@ module Kward
|
|
|
64
64
|
content = File.read(resolved)
|
|
65
65
|
return "Error: not a text file: #{path}" if binary_content?(content)
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
read_mode = normalize_read_mode(mode)
|
|
68
|
+
return read_mode if read_mode.is_a?(String)
|
|
69
|
+
|
|
70
|
+
output_budget = read_output_budget(max_bytes)
|
|
71
|
+
return output_budget if output_budget.is_a?(String)
|
|
72
|
+
|
|
73
|
+
case read_mode
|
|
74
|
+
when :outline
|
|
75
|
+
file_structure_summary(path, content)
|
|
76
|
+
when :preview
|
|
77
|
+
read_file_slice(content, offset: offset, limit: limit || 120, max_bytes: output_budget)
|
|
78
|
+
when :range
|
|
79
|
+
read_file_slice(content, offset: offset, limit: limit, max_bytes: output_budget)
|
|
80
|
+
when :full
|
|
81
|
+
read_file_slice(content, offset: offset, limit: limit, max_bytes: output_budget)
|
|
82
|
+
else
|
|
83
|
+
large_file_outline_response(path, content, offset: offset, limit: limit) || read_file_slice(content, offset: offset, limit: limit, max_bytes: output_budget)
|
|
84
|
+
end
|
|
85
|
+
rescue SecurityError, Errno::ENOENT => e
|
|
86
|
+
"Error: #{e.message}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Returns a compact outline of recognizable source-code declarations.
|
|
90
|
+
def summarize_file_structure(path)
|
|
91
|
+
resolved = workspace_path(path)
|
|
92
|
+
return "Error: not a file: #{path}" unless File.file?(resolved)
|
|
93
|
+
|
|
94
|
+
size = File.size(resolved)
|
|
95
|
+
return "Error: file too large: #{path} is #{size} bytes; limit is #{@max_file_bytes} bytes" if size > @max_file_bytes
|
|
96
|
+
|
|
97
|
+
content = File.read(resolved)
|
|
98
|
+
return "Error: not a text file: #{path}" if binary_content?(content)
|
|
99
|
+
|
|
100
|
+
file_structure_summary(path, content)
|
|
68
101
|
rescue SecurityError, Errno::ENOENT => e
|
|
69
102
|
"Error: #{e.message}"
|
|
70
103
|
end
|
|
@@ -196,7 +229,115 @@ module Kward
|
|
|
196
229
|
Pathname.new(path).relative_path_from(@root).to_s
|
|
197
230
|
end
|
|
198
231
|
|
|
199
|
-
def
|
|
232
|
+
def large_file_outline_response(path, content, offset:, limit:)
|
|
233
|
+
return nil unless offset.nil? && limit.nil?
|
|
234
|
+
lines = content.split("\n", -1)
|
|
235
|
+
return nil unless lines.length > @max_read_output_lines || content.bytesize > @max_read_output_bytes
|
|
236
|
+
|
|
237
|
+
outline = source_outline(lines)
|
|
238
|
+
return nil if outline.empty?
|
|
239
|
+
|
|
240
|
+
preview_limit = [120, @max_read_output_lines].min
|
|
241
|
+
preview = lines.first(preview_limit).join("\n")
|
|
242
|
+
[
|
|
243
|
+
"File has #{lines.length} lines (#{content.bytesize} bytes). Showing an outline and the first #{preview_limit} lines to reduce model context.",
|
|
244
|
+
"",
|
|
245
|
+
"Outline:",
|
|
246
|
+
outline.join("\n"),
|
|
247
|
+
"",
|
|
248
|
+
"First #{preview_limit} lines:",
|
|
249
|
+
preview,
|
|
250
|
+
"",
|
|
251
|
+
"[Use read_file with mode=\"range\", offset=#{preview_limit + 1}, and limit to continue; mode=\"outline\" for only the outline; or request a specific section from the outline.]"
|
|
252
|
+
].join("\n")
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def file_structure_summary(path, content)
|
|
256
|
+
lines = content.split("\n", -1)
|
|
257
|
+
outline = source_outline(lines)
|
|
258
|
+
return "No recognizable source structure found in #{path}." if outline.empty?
|
|
259
|
+
|
|
260
|
+
(["# File structure: #{path}", "- Lines: #{lines.length}", "- Bytes: #{content.bytesize}", "", "## Outline"] + outline).join("\n")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def source_outline(lines)
|
|
264
|
+
entries = source_outline_entries(lines)
|
|
265
|
+
entries.first(80).map do |entry|
|
|
266
|
+
range = entry[:end_line] && entry[:end_line] != entry[:line] ? " (range #{entry[:line]}-#{entry[:end_line]}, #{entry[:kind]})" : " (#{entry[:kind]})"
|
|
267
|
+
"line #{entry[:line]}: #{' ' * [entry[:indent] / 2, 6].min}#{entry[:signature]}#{range}"
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def source_outline_entries(lines)
|
|
272
|
+
candidates = []
|
|
273
|
+
lines.each_with_index do |line, index|
|
|
274
|
+
declaration = source_declaration(line.strip)
|
|
275
|
+
next unless declaration
|
|
276
|
+
|
|
277
|
+
candidates << declaration.merge(line: index + 1, indent: line[/\A\s*/].to_s.length)
|
|
278
|
+
end
|
|
279
|
+
candidates.each_with_index do |entry, index|
|
|
280
|
+
following = candidates[(index + 1)..]&.find { |candidate| candidate[:indent] <= entry[:indent] }
|
|
281
|
+
entry[:end_line] = following ? following[:line] - 1 : last_content_line(lines)
|
|
282
|
+
end
|
|
283
|
+
candidates
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def source_declaration(stripped)
|
|
287
|
+
case stripped
|
|
288
|
+
when /\A(module)\s+(.+)/
|
|
289
|
+
{ kind: "module", signature: stripped }
|
|
290
|
+
when /\A(class)\s+(.+)/
|
|
291
|
+
{ kind: "class", signature: stripped }
|
|
292
|
+
when /\A(async\s+def|def)\s+(.+)/
|
|
293
|
+
{ kind: "function", signature: stripped }
|
|
294
|
+
when /\A(export\s+)?(async\s+)?function\s+(.+)/
|
|
295
|
+
{ kind: "function", signature: stripped }
|
|
296
|
+
when /\A(async\s+)?(?:get\s+|set\s+)?(?:constructor|[A-Za-z_$][\w$]*)\s*\([^;]*\)\s*(?::\s*[^{}]+)?\s*(?:\{\}|\{|=>)?\z/
|
|
297
|
+
{ kind: "method", signature: stripped } unless stripped.match?(/\A(if|for|while|switch|catch)\b/)
|
|
298
|
+
when /\A(export\s+)?(class|interface|type|enum)\s+(.+)/
|
|
299
|
+
{ kind: Regexp.last_match(2), signature: stripped }
|
|
300
|
+
when /\A(?:export\s+)?(?:const|let|var)\s+\w+\s*=.*=>/
|
|
301
|
+
{ kind: "function", signature: stripped }
|
|
302
|
+
when /\Afunc\s+(.+)/
|
|
303
|
+
{ kind: "function", signature: stripped }
|
|
304
|
+
when /\Atype\s+\w+\s+(struct|interface)\b/
|
|
305
|
+
{ kind: Regexp.last_match(1), signature: stripped }
|
|
306
|
+
when /\A(pub\s+)?(async\s+)?fn\s+(.+)/
|
|
307
|
+
{ kind: "function", signature: stripped }
|
|
308
|
+
when /\A(pub\s+)?(struct|enum|trait|impl)\b(.+)?/
|
|
309
|
+
{ kind: Regexp.last_match(2), signature: stripped }
|
|
310
|
+
when /\A(?:public|private|protected|internal|static|final|abstract|async|override|virtual|sealed|readonly|partial|\s)+\s*(class|interface|enum|record)\s+(.+)/
|
|
311
|
+
{ kind: Regexp.last_match(1), signature: stripped }
|
|
312
|
+
when /\A(?:public|private|protected|internal|static|final|abstract|async|override|virtual|sealed|readonly|partial|\s)+\s*\S[^{;=]*\w+\s*\([^;]*\)\s*(?:\{|=>)?\z/
|
|
313
|
+
{ kind: "method", signature: stripped }
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def last_content_line(lines)
|
|
318
|
+
index = lines.rindex { |line| !line.strip.empty? }
|
|
319
|
+
index ? index + 1 : lines.length
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def normalize_read_mode(mode)
|
|
323
|
+
return nil if mode.nil? || mode.to_s.empty?
|
|
324
|
+
|
|
325
|
+
value = mode.to_s.downcase
|
|
326
|
+
return value.to_sym if %w[preview outline range full].include?(value)
|
|
327
|
+
|
|
328
|
+
"Error: mode must be one of preview, outline, range, full"
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def read_output_budget(max_bytes)
|
|
332
|
+
return @max_read_output_bytes if max_bytes.nil?
|
|
333
|
+
|
|
334
|
+
value = max_bytes.to_i
|
|
335
|
+
return "Error: max_bytes must be positive" unless value.positive?
|
|
336
|
+
|
|
337
|
+
[value, @max_read_output_bytes].min
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def read_file_slice(content, offset:, limit:, max_bytes: @max_read_output_bytes)
|
|
200
341
|
lines = content.split("\n", -1)
|
|
201
342
|
lines = [""] if lines.empty?
|
|
202
343
|
start_index = read_start_index(offset)
|
|
@@ -207,7 +348,7 @@ module Kward
|
|
|
207
348
|
|
|
208
349
|
selected_end = user_limit ? [start_index + user_limit, lines.length].min : lines.length
|
|
209
350
|
selected_lines = lines[start_index...selected_end]
|
|
210
|
-
truncated = truncate_read_lines(selected_lines)
|
|
351
|
+
truncated = truncate_read_lines(selected_lines, max_bytes: max_bytes)
|
|
211
352
|
return truncated[:error] if truncated[:error]
|
|
212
353
|
|
|
213
354
|
output = truncated[:content]
|
|
@@ -216,7 +357,8 @@ module Kward
|
|
|
216
357
|
start_index: start_index,
|
|
217
358
|
output_lines: truncated[:line_count],
|
|
218
359
|
total_lines: lines.length,
|
|
219
|
-
truncated_by: truncated[:truncated_by]
|
|
360
|
+
truncated_by: truncated[:truncated_by],
|
|
361
|
+
max_bytes: max_bytes
|
|
220
362
|
)
|
|
221
363
|
elsif user_limit && selected_end < lines.length
|
|
222
364
|
output << "\n\n[#{lines.length - selected_end} more lines in file. Use offset=#{selected_end + 1} to continue.]"
|
|
@@ -244,11 +386,11 @@ module Kward
|
|
|
244
386
|
value
|
|
245
387
|
end
|
|
246
388
|
|
|
247
|
-
def truncate_read_lines(lines)
|
|
389
|
+
def truncate_read_lines(lines, max_bytes: @max_read_output_bytes)
|
|
248
390
|
first_line = lines.first.to_s
|
|
249
|
-
if first_line.bytesize >
|
|
391
|
+
if first_line.bytesize > max_bytes
|
|
250
392
|
return {
|
|
251
|
-
error: "Error: first line is #{first_line.bytesize} bytes, exceeds #{
|
|
393
|
+
error: "Error: first line is #{first_line.bytesize} bytes, exceeds #{max_bytes} byte read limit. Use run_shell_command with sed/head to inspect smaller chunks."
|
|
252
394
|
}
|
|
253
395
|
end
|
|
254
396
|
|
|
@@ -263,7 +405,7 @@ module Kward
|
|
|
263
405
|
|
|
264
406
|
separator_bytes = output_lines.empty? ? 0 : 1
|
|
265
407
|
next_bytes = line.bytesize + separator_bytes
|
|
266
|
-
if bytes + next_bytes >
|
|
408
|
+
if bytes + next_bytes > max_bytes
|
|
267
409
|
truncated_by = "bytes"
|
|
268
410
|
break
|
|
269
411
|
end
|
|
@@ -280,10 +422,10 @@ module Kward
|
|
|
280
422
|
}
|
|
281
423
|
end
|
|
282
424
|
|
|
283
|
-
def read_truncation_notice(start_index:, output_lines:, total_lines:, truncated_by:)
|
|
425
|
+
def read_truncation_notice(start_index:, output_lines:, total_lines:, truncated_by:, max_bytes: @max_read_output_bytes)
|
|
284
426
|
end_line = start_index + output_lines
|
|
285
427
|
next_offset = end_line + 1
|
|
286
|
-
detail = truncated_by == "lines" ? "#{@max_read_output_lines} line limit" : "#{
|
|
428
|
+
detail = truncated_by == "lines" ? "#{@max_read_output_lines} line limit" : "#{max_bytes} byte limit"
|
|
287
429
|
"\n\n[Showing lines #{start_index + 1}-#{end_line} of #{total_lines} (#{detail}). Use offset=#{next_offset} to continue.]"
|
|
288
430
|
end
|
|
289
431
|
|