kward 0.71.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/CHANGELOG.md +41 -1
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- 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 +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- 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 +387 -35
- 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/question_prompt.rb +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- 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 +44 -0
- 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/text_boundary.rb +25 -0
- 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 +62 -16
- data/lib/kward/tools/tool_call.rb +10 -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 +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require "securerandom"
|
|
2
|
+
require "time"
|
|
3
|
+
require_relative "../config_files"
|
|
4
|
+
require_relative "../cancellation"
|
|
5
|
+
|
|
6
|
+
module Kward
|
|
7
|
+
module Workers
|
|
8
|
+
# Runtime record for one independent unit of agent work.
|
|
9
|
+
class Worker
|
|
10
|
+
STATUSES = %w[idle queued running ready failed cancelled archived].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(id: SecureRandom.hex(4), title:, role:, workspace_root: Dir.pwd, status: "idle", prompt: nil, conversation: nil, session: nil, cancellation: Cancellation.new, created_at: Time.now.utc)
|
|
13
|
+
@id = id
|
|
14
|
+
@title = title.to_s
|
|
15
|
+
@role = role.to_s
|
|
16
|
+
@workspace_root = ConfigFiles.canonical_workspace_root(workspace_root)
|
|
17
|
+
@status = status.to_s
|
|
18
|
+
@prompt = prompt.to_s
|
|
19
|
+
@conversation = conversation
|
|
20
|
+
@session = session
|
|
21
|
+
@cancellation = cancellation
|
|
22
|
+
@created_at = created_at
|
|
23
|
+
@updated_at = created_at
|
|
24
|
+
@started_at = nil
|
|
25
|
+
@finished_at = nil
|
|
26
|
+
@report = nil
|
|
27
|
+
@error = nil
|
|
28
|
+
@thread = nil
|
|
29
|
+
@event_history = []
|
|
30
|
+
@event_queue = Queue.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
attr_reader :id, :title, :role, :workspace_root, :prompt, :conversation, :session, :cancellation, :created_at, :updated_at, :started_at, :finished_at, :report, :error, :thread, :event_history, :event_queue
|
|
34
|
+
attr_writer :conversation, :session, :thread
|
|
35
|
+
|
|
36
|
+
def status
|
|
37
|
+
@status
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def update_status(status, error: nil, report: nil)
|
|
41
|
+
@status = status.to_s
|
|
42
|
+
@error = error unless error.nil?
|
|
43
|
+
@report = report unless report.nil?
|
|
44
|
+
now = Time.now.utc
|
|
45
|
+
@updated_at = now
|
|
46
|
+
@started_at ||= now if @status == "running"
|
|
47
|
+
@finished_at = now if %w[ready failed cancelled archived].include?(@status)
|
|
48
|
+
self
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def record_event(event)
|
|
52
|
+
@event_history << event
|
|
53
|
+
@event_queue << event
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def to_h
|
|
57
|
+
{
|
|
58
|
+
"id" => id,
|
|
59
|
+
"title" => title,
|
|
60
|
+
"role" => role,
|
|
61
|
+
"status" => status,
|
|
62
|
+
"prompt" => prompt,
|
|
63
|
+
"workspace_root" => workspace_root,
|
|
64
|
+
"session_id" => session&.id,
|
|
65
|
+
"session_path" => session&.path,
|
|
66
|
+
"created_at" => timestamp(created_at),
|
|
67
|
+
"updated_at" => timestamp(updated_at),
|
|
68
|
+
"started_at" => timestamp(started_at),
|
|
69
|
+
"finished_at" => timestamp(finished_at),
|
|
70
|
+
"report" => report,
|
|
71
|
+
"error" => error
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def timestamp(value)
|
|
78
|
+
value&.utc&.iso8601(3)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
3
|
+
module Kward
|
|
4
|
+
module Workers
|
|
5
|
+
# Cooperative ownership guard for workspace-mutating worker tools.
|
|
6
|
+
class WriteLock
|
|
7
|
+
def initialize
|
|
8
|
+
@owner_id = nil
|
|
9
|
+
@mutex = Mutex.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :owner_id
|
|
13
|
+
|
|
14
|
+
def acquire(owner_id)
|
|
15
|
+
owner = owner_id.to_s
|
|
16
|
+
return false if owner.empty?
|
|
17
|
+
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
return true if @owner_id == owner
|
|
20
|
+
return false if @owner_id
|
|
21
|
+
|
|
22
|
+
@owner_id = owner
|
|
23
|
+
true
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def owned_by?(owner_id)
|
|
28
|
+
@mutex.synchronize { @owner_id == owner_id.to_s }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def release(owner_id)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
@owner_id = nil if @owner_id == owner_id.to_s
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/kward/workspace.rb
CHANGED
|
@@ -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,24 @@ 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
|
|
68
85
|
rescue SecurityError, Errno::ENOENT => e
|
|
69
86
|
"Error: #{e.message}"
|
|
70
87
|
end
|
|
@@ -80,11 +97,7 @@ module Kward
|
|
|
80
97
|
content = File.read(resolved)
|
|
81
98
|
return "Error: not a text file: #{path}" if binary_content?(content)
|
|
82
99
|
|
|
83
|
-
|
|
84
|
-
outline = source_outline(lines)
|
|
85
|
-
return "No recognizable source structure found in #{path}." if outline.empty?
|
|
86
|
-
|
|
87
|
-
(["# File structure: #{path}", "- Lines: #{lines.length}", "- Bytes: #{content.bytesize}", "", "## Outline"] + outline).join("\n")
|
|
100
|
+
file_structure_summary(path, content)
|
|
88
101
|
rescue SecurityError, Errno::ENOENT => e
|
|
89
102
|
"Error: #{e.message}"
|
|
90
103
|
end
|
|
@@ -235,24 +248,96 @@ module Kward
|
|
|
235
248
|
"First #{preview_limit} lines:",
|
|
236
249
|
preview,
|
|
237
250
|
"",
|
|
238
|
-
"[Use read_file with offset=#{preview_limit + 1} and limit to continue
|
|
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.]"
|
|
239
252
|
].join("\n")
|
|
240
253
|
end
|
|
241
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
|
+
|
|
242
263
|
def source_outline(lines)
|
|
243
|
-
|
|
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 = []
|
|
244
273
|
lines.each_with_index do |line, index|
|
|
245
|
-
|
|
246
|
-
next unless
|
|
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
|
|
247
285
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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 }
|
|
251
314
|
end
|
|
252
|
-
outline
|
|
253
315
|
end
|
|
254
316
|
|
|
255
|
-
def
|
|
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)
|
|
256
341
|
lines = content.split("\n", -1)
|
|
257
342
|
lines = [""] if lines.empty?
|
|
258
343
|
start_index = read_start_index(offset)
|
|
@@ -263,7 +348,7 @@ module Kward
|
|
|
263
348
|
|
|
264
349
|
selected_end = user_limit ? [start_index + user_limit, lines.length].min : lines.length
|
|
265
350
|
selected_lines = lines[start_index...selected_end]
|
|
266
|
-
truncated = truncate_read_lines(selected_lines)
|
|
351
|
+
truncated = truncate_read_lines(selected_lines, max_bytes: max_bytes)
|
|
267
352
|
return truncated[:error] if truncated[:error]
|
|
268
353
|
|
|
269
354
|
output = truncated[:content]
|
|
@@ -272,7 +357,8 @@ module Kward
|
|
|
272
357
|
start_index: start_index,
|
|
273
358
|
output_lines: truncated[:line_count],
|
|
274
359
|
total_lines: lines.length,
|
|
275
|
-
truncated_by: truncated[:truncated_by]
|
|
360
|
+
truncated_by: truncated[:truncated_by],
|
|
361
|
+
max_bytes: max_bytes
|
|
276
362
|
)
|
|
277
363
|
elsif user_limit && selected_end < lines.length
|
|
278
364
|
output << "\n\n[#{lines.length - selected_end} more lines in file. Use offset=#{selected_end + 1} to continue.]"
|
|
@@ -300,11 +386,11 @@ module Kward
|
|
|
300
386
|
value
|
|
301
387
|
end
|
|
302
388
|
|
|
303
|
-
def truncate_read_lines(lines)
|
|
389
|
+
def truncate_read_lines(lines, max_bytes: @max_read_output_bytes)
|
|
304
390
|
first_line = lines.first.to_s
|
|
305
|
-
if first_line.bytesize >
|
|
391
|
+
if first_line.bytesize > max_bytes
|
|
306
392
|
return {
|
|
307
|
-
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."
|
|
308
394
|
}
|
|
309
395
|
end
|
|
310
396
|
|
|
@@ -319,7 +405,7 @@ module Kward
|
|
|
319
405
|
|
|
320
406
|
separator_bytes = output_lines.empty? ? 0 : 1
|
|
321
407
|
next_bytes = line.bytesize + separator_bytes
|
|
322
|
-
if bytes + next_bytes >
|
|
408
|
+
if bytes + next_bytes > max_bytes
|
|
323
409
|
truncated_by = "bytes"
|
|
324
410
|
break
|
|
325
411
|
end
|
|
@@ -336,10 +422,10 @@ module Kward
|
|
|
336
422
|
}
|
|
337
423
|
end
|
|
338
424
|
|
|
339
|
-
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)
|
|
340
426
|
end_line = start_index + output_lines
|
|
341
427
|
next_offset = end_line + 1
|
|
342
|
-
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"
|
|
343
429
|
"\n\n[Showing lines #{start_index + 1}-#{end_line} of #{total_lines} (#{detail}). Use offset=#{next_offset} to continue.]"
|
|
344
430
|
end
|
|
345
431
|
|
|
@@ -341,7 +341,41 @@ body.kward-docs a {
|
|
|
341
341
|
color: var(--kward-accent-bright);
|
|
342
342
|
}
|
|
343
343
|
|
|
344
|
-
body.kward-docs
|
|
344
|
+
body.kward-docs #content quote,
|
|
345
|
+
body.kward-docs #filecontents quote {
|
|
346
|
+
border-left: 3px solid var(--kward-accent);
|
|
347
|
+
color: var(--kward-ink);
|
|
348
|
+
display: block;
|
|
349
|
+
font-style: italic;
|
|
350
|
+
font-size: 18px;
|
|
351
|
+
line-height: 1.8;
|
|
352
|
+
margin: 28px 0;
|
|
353
|
+
padding: 8px 0 8px 46px;
|
|
354
|
+
position: relative;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
body.kward-docs #content quote::before,
|
|
358
|
+
body.kward-docs #filecontents quote::before {
|
|
359
|
+
color: var(--kward-accent);
|
|
360
|
+
content: "\201C";
|
|
361
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
362
|
+
font-size: 80px;
|
|
363
|
+
font-style: normal;
|
|
364
|
+
left: 4px;
|
|
365
|
+
line-height: 1;
|
|
366
|
+
opacity: 0.55;
|
|
367
|
+
position: absolute;
|
|
368
|
+
top: -10px;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
body.kward-docs #content quote br,
|
|
372
|
+
body.kward-docs #filecontents quote br {
|
|
373
|
+
content: "";
|
|
374
|
+
display: block;
|
|
375
|
+
margin-bottom: 4px;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
body.kward-docs #content pre {
|
|
345
379
|
background: var(--kward-code);
|
|
346
380
|
border: 1px solid rgba(149, 169, 52, 0.22);
|
|
347
381
|
border-radius: 10px;
|
|
@@ -537,11 +571,10 @@ body.kward-docs #footer {
|
|
|
537
571
|
}
|
|
538
572
|
|
|
539
573
|
/*
|
|
540
|
-
Hero tagline "swosh":
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
the left — both lines swap in sync. Pure CSS — no JS. The global
|
|
574
|
+
Hero tagline "swosh": cycles each line through three phrases. The first
|
|
575
|
+
line moves right-to-left: "Your terminal.", "Your RPC frontend.", then
|
|
576
|
+
"Your workflow." The second line mirrors it left-to-right: "Your agent.",
|
|
577
|
+
"Your LLM engine.", then "Your harness." Pure CSS — no JS. The global
|
|
545
578
|
@media (prefers-reduced-motion) rule above collapses the animation to
|
|
546
579
|
the static original copy ("Your terminal. Your agent.") so it remains
|
|
547
580
|
accessible and calm.
|
|
@@ -566,68 +599,95 @@ body.kward-docs #footer {
|
|
|
566
599
|
will-change: transform, opacity;
|
|
567
600
|
}
|
|
568
601
|
|
|
569
|
-
/* The
|
|
570
|
-
the shorter
|
|
571
|
-
.kward-swosh-text-a
|
|
602
|
+
/* The longest text ("Your RPC frontend.") stays in flow to size the box;
|
|
603
|
+
the shorter phrases overlay it absolutely. */
|
|
604
|
+
.kward-swosh-text-a,
|
|
605
|
+
.kward-swosh-text-e {
|
|
572
606
|
left: 0.08em;
|
|
573
607
|
position: absolute;
|
|
574
608
|
top: 0.16em;
|
|
575
609
|
}
|
|
576
610
|
|
|
577
611
|
.kward-swosh-text-a {
|
|
578
|
-
animation: kward-swosh-a
|
|
612
|
+
animation: kward-swosh-a 18s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
|
|
579
613
|
}
|
|
580
614
|
|
|
581
615
|
.kward-swosh-text-b {
|
|
582
|
-
animation: kward-swosh-b
|
|
616
|
+
animation: kward-swosh-b 18s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
|
|
583
617
|
position: relative;
|
|
584
618
|
}
|
|
585
619
|
|
|
620
|
+
.kward-swosh-text-e {
|
|
621
|
+
animation: kward-swosh-e 18s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
|
|
622
|
+
}
|
|
623
|
+
|
|
586
624
|
@keyframes kward-swosh-a {
|
|
587
|
-
0%,
|
|
588
|
-
|
|
589
|
-
|
|
625
|
+
0%, 25% { transform: translateX(0); opacity: 1; filter: none; }
|
|
626
|
+
33% { transform: translateX(-140%); opacity: 0; filter: blur(3px); }
|
|
627
|
+
34%, 92% { transform: translateX(140%); opacity: 0; filter: none; }
|
|
590
628
|
100% { transform: translateX(0); opacity: 1; filter: none; }
|
|
591
629
|
}
|
|
592
630
|
|
|
593
631
|
@keyframes kward-swosh-b {
|
|
594
|
-
0%,
|
|
595
|
-
|
|
596
|
-
|
|
632
|
+
0%, 25% { transform: translateX(140%); opacity: 0; filter: none; }
|
|
633
|
+
33% { transform: translateX(0); opacity: 1; filter: none; }
|
|
634
|
+
58% { transform: translateX(0); opacity: 1; filter: none; }
|
|
635
|
+
66% { transform: translateX(-140%); opacity: 0; filter: blur(3px); }
|
|
636
|
+
67%, 100% { transform: translateX(140%); opacity: 0; filter: none; }
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
@keyframes kward-swosh-e {
|
|
640
|
+
0%, 58% { transform: translateX(140%); opacity: 0; filter: none; }
|
|
641
|
+
66% { transform: translateX(0); opacity: 1; filter: none; }
|
|
642
|
+
92% { transform: translateX(0); opacity: 1; filter: none; }
|
|
597
643
|
100% { transform: translateX(-140%); opacity: 0; filter: blur(3px); }
|
|
598
644
|
}
|
|
599
645
|
|
|
600
646
|
/*
|
|
601
|
-
Second line, mirrored
|
|
602
|
-
|
|
603
|
-
the first swosh. Both texts keep the accent-bright color of the
|
|
604
|
-
original "Your agent." span.
|
|
647
|
+
Second line, mirrored left-to-right. All texts keep the accent-bright
|
|
648
|
+
color of the original "Your agent." span.
|
|
605
649
|
*/
|
|
606
|
-
.kward-hero h1 .kward-swosh-text-c
|
|
607
|
-
|
|
650
|
+
.kward-hero h1 .kward-swosh-text-c,
|
|
651
|
+
.kward-hero h1 .kward-swosh-text-f {
|
|
608
652
|
color: var(--kward-accent-bright);
|
|
609
653
|
left: 0.08em;
|
|
610
654
|
position: absolute;
|
|
611
655
|
top: 0.16em;
|
|
612
656
|
}
|
|
613
657
|
|
|
658
|
+
.kward-hero h1 .kward-swosh-text-c {
|
|
659
|
+
animation: kward-swosh-c 18s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
|
|
660
|
+
}
|
|
661
|
+
|
|
614
662
|
.kward-hero h1 .kward-swosh-text-d {
|
|
615
|
-
animation: kward-swosh-d
|
|
663
|
+
animation: kward-swosh-d 18s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
|
|
616
664
|
color: var(--kward-accent-bright);
|
|
617
665
|
position: relative;
|
|
618
666
|
}
|
|
619
667
|
|
|
668
|
+
.kward-hero h1 .kward-swosh-text-f {
|
|
669
|
+
animation: kward-swosh-f 18s cubic-bezier(0.22, 0.61, 0.36, 1) infinite;
|
|
670
|
+
}
|
|
671
|
+
|
|
620
672
|
@keyframes kward-swosh-c {
|
|
621
|
-
0%,
|
|
622
|
-
|
|
623
|
-
|
|
673
|
+
0%, 25% { transform: translateX(0); opacity: 1; filter: none; }
|
|
674
|
+
33% { transform: translateX(140%); opacity: 0; filter: blur(3px); }
|
|
675
|
+
34%, 92% { transform: translateX(-140%); opacity: 0; filter: none; }
|
|
624
676
|
100% { transform: translateX(0); opacity: 1; filter: none; }
|
|
625
677
|
}
|
|
626
678
|
|
|
627
679
|
@keyframes kward-swosh-d {
|
|
628
|
-
0%,
|
|
629
|
-
|
|
630
|
-
|
|
680
|
+
0%, 25% { transform: translateX(-140%); opacity: 0; filter: none; }
|
|
681
|
+
33% { transform: translateX(0); opacity: 1; filter: none; }
|
|
682
|
+
58% { transform: translateX(0); opacity: 1; filter: none; }
|
|
683
|
+
66% { transform: translateX(140%); opacity: 0; filter: blur(3px); }
|
|
684
|
+
67%, 100% { transform: translateX(-140%); opacity: 0; filter: none; }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
@keyframes kward-swosh-f {
|
|
688
|
+
0%, 58% { transform: translateX(-140%); opacity: 0; filter: none; }
|
|
689
|
+
66% { transform: translateX(0); opacity: 1; filter: none; }
|
|
690
|
+
92% { transform: translateX(0); opacity: 1; filter: none; }
|
|
631
691
|
100% { transform: translateX(140%); opacity: 0; filter: blur(3px); }
|
|
632
692
|
}
|
|
633
693
|
|
|
@@ -1193,7 +1253,7 @@ body.kward-docs .kward-home #footer {
|
|
|
1193
1253
|
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.48);
|
|
1194
1254
|
display: none;
|
|
1195
1255
|
gap: 16px;
|
|
1196
|
-
grid-template-columns: repeat(
|
|
1256
|
+
grid-template-columns: repeat(3, minmax(160px, 220px));
|
|
1197
1257
|
left: 50%;
|
|
1198
1258
|
max-width: calc(100vw - 48px);
|
|
1199
1259
|
opacity: 0;
|
|
@@ -1549,17 +1609,14 @@ body.kward-docs #main.kward-home {
|
|
|
1549
1609
|
justify-content: center;
|
|
1550
1610
|
}
|
|
1551
1611
|
|
|
1552
|
-
/* The generated YARD TOC is
|
|
1553
|
-
.kward-guide-page #toc {
|
|
1554
|
-
display: none;
|
|
1555
|
-
}
|
|
1612
|
+
/* The generated YARD TOC is shown on all docs pages. Use the `kward-no-toc` marker to suppress it per page. */
|
|
1556
1613
|
|
|
1557
1614
|
|
|
1558
1615
|
/* Code blocks own their copy action. */
|
|
1559
1616
|
body.kward-docs .code-copy-wrapper {
|
|
1617
|
+
display: flow-root;
|
|
1560
1618
|
margin: 16px 0 24px;
|
|
1561
1619
|
position: relative;
|
|
1562
|
-
width: 100%;
|
|
1563
1620
|
}
|
|
1564
1621
|
|
|
1565
1622
|
body.kward-docs .code-copy-wrapper pre,
|
|
@@ -1571,11 +1628,14 @@ body.kward-docs .code-copy-wrapper pre.code {
|
|
|
1571
1628
|
}
|
|
1572
1629
|
|
|
1573
1630
|
body.kward-docs .code-copy-wrapper .copy-code-button {
|
|
1631
|
+
background: #020503;
|
|
1632
|
+
box-shadow: 0 0 0 4px #020503;
|
|
1574
1633
|
float: none;
|
|
1575
1634
|
margin: 0;
|
|
1576
1635
|
position: absolute;
|
|
1577
1636
|
right: 12px;
|
|
1578
1637
|
top: 12px;
|
|
1638
|
+
z-index: 2;
|
|
1579
1639
|
}
|
|
1580
1640
|
|
|
1581
1641
|
|
|
@@ -1986,6 +2046,17 @@ body.kward-docs ul.toplevel a:active {
|
|
|
1986
2046
|
color: #000;
|
|
1987
2047
|
}
|
|
1988
2048
|
|
|
2049
|
+
body.kward-docs #content quote,
|
|
2050
|
+
body.kward-docs #filecontents quote {
|
|
2051
|
+
border-left-color: #999;
|
|
2052
|
+
color: #000;
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
body.kward-docs #content quote::before,
|
|
2056
|
+
body.kward-docs #filecontents quote::before {
|
|
2057
|
+
color: #999;
|
|
2058
|
+
}
|
|
2059
|
+
|
|
1989
2060
|
.kward-page {
|
|
1990
2061
|
padding: 0;
|
|
1991
2062
|
max-width: none;
|
|
@@ -16,9 +16,19 @@ module KwardDocsNavigationData
|
|
|
16
16
|
"Feature guides",
|
|
17
17
|
[
|
|
18
18
|
["Sessions", "file.session-management.html"],
|
|
19
|
+
["Tabs", "file.tabs.html"],
|
|
19
20
|
["Memory", "file.memory.html"],
|
|
20
21
|
["Personas", "file.personas.html"]
|
|
21
22
|
]
|
|
23
|
+
],
|
|
24
|
+
[
|
|
25
|
+
"User Tools",
|
|
26
|
+
[
|
|
27
|
+
["Project files", "file.files.html"],
|
|
28
|
+
["Integrated Editor", "file.editor.html"],
|
|
29
|
+
["Git", "file.git.html"],
|
|
30
|
+
["Shell", "file.shell.html"]
|
|
31
|
+
]
|
|
22
32
|
]
|
|
23
33
|
].freeze
|
|
24
34
|
|
|
@@ -44,7 +54,8 @@ module KwardDocsNavigationData
|
|
|
44
54
|
["Workspace tools", "file.workspace-tools.html"],
|
|
45
55
|
["Web search", "file.web-search.html"],
|
|
46
56
|
["Code search", "file.code-search.html"],
|
|
47
|
-
["Context tools", "file.context-tools.html"]
|
|
57
|
+
["Context tools", "file.context-tools.html"],
|
|
58
|
+
["Context budgeting", "file.context-budgeting.html"]
|
|
48
59
|
]
|
|
49
60
|
]
|
|
50
61
|
].freeze
|
|
@@ -82,14 +82,16 @@
|
|
|
82
82
|
<div class="kward-hero-copy">
|
|
83
83
|
<p class="kward-eyebrow">⌘ Ruby CLI Coding Agent</p>
|
|
84
84
|
<h1>
|
|
85
|
-
<span class="kward-swosh" aria-label="Your terminal.">
|
|
85
|
+
<span class="kward-swosh" aria-label="Your terminal. Your RPC frontend. Your workflow.">
|
|
86
86
|
<span class="kward-swosh-text kward-swosh-text-a" aria-hidden="true">Your terminal.</span>
|
|
87
87
|
<span class="kward-swosh-text kward-swosh-text-b" aria-hidden="true">Your RPC frontend.</span>
|
|
88
|
+
<span class="kward-swosh-text kward-swosh-text-e" aria-hidden="true">Your workflow.</span>
|
|
88
89
|
</span>
|
|
89
90
|
<br>
|
|
90
|
-
<span class="kward-swosh" aria-label="Your agent.">
|
|
91
|
+
<span class="kward-swosh" aria-label="Your agent. Your LLM engine. Your harness.">
|
|
91
92
|
<span class="kward-swosh-text kward-swosh-text-c" aria-hidden="true">Your agent.</span>
|
|
92
93
|
<span class="kward-swosh-text kward-swosh-text-d" aria-hidden="true">Your LLM engine.</span>
|
|
94
|
+
<span class="kward-swosh-text kward-swosh-text-f" aria-hidden="true">Your harness.</span>
|
|
93
95
|
</span>
|
|
94
96
|
</h1>
|
|
95
97
|
<p class="kward-lede">Kward is an extendable Ruby CLI coding agent that helps you understand your project, edit files, run commands, search the web, and automate workflows—right from your terminal.</p>
|
|
@@ -31,12 +31,18 @@ module KwardDocsNavigation
|
|
|
31
31
|
"doc/authentication.md" => "file.authentication.html",
|
|
32
32
|
"doc/troubleshooting.md" => "file.troubleshooting.html",
|
|
33
33
|
"doc/session-management.md" => "file.session-management.html",
|
|
34
|
+
"doc/tabs.md" => "file.tabs.html",
|
|
35
|
+
"doc/files.md" => "file.files.html",
|
|
36
|
+
"doc/editor.md" => "file.editor.html",
|
|
37
|
+
"doc/git.md" => "file.git.html",
|
|
38
|
+
"doc/shell.md" => "file.shell.html",
|
|
34
39
|
"doc/memory.md" => "file.memory.html",
|
|
35
40
|
"doc/personas.md" => "file.personas.html",
|
|
36
41
|
"doc/extensibility.md" => "file.extensibility.html",
|
|
37
42
|
"doc/plugins.md" => "file.plugins.html",
|
|
38
43
|
"doc/agent-tools.md" => "file.agent-tools.html",
|
|
39
44
|
"doc/workspace-tools.md" => "file.workspace-tools.html",
|
|
45
|
+
"doc/context-budgeting.md" => "file.context-budgeting.html",
|
|
40
46
|
"doc/web-search.md" => "file.web-search.html",
|
|
41
47
|
"doc/code-search.md" => "file.code-search.html",
|
|
42
48
|
"doc/context-tools.md" => "file.context-tools.html",
|