kward 0.71.0 → 0.73.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/ci.yml +30 -0
- data/CHANGELOG.md +93 -0
- 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 +415 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +123 -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 +75 -5
- data/doc/session-management.md +35 -1
- data/doc/shell.md +332 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +79 -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/ansi.rb +62 -23
- 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/rendering.rb +4 -1
- data/lib/kward/cli/runtime_helpers.rb +268 -4
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +217 -9
- data/lib/kward/cli/slash_commands.rb +628 -2
- data/lib/kward/cli/tabs.rb +725 -0
- data/lib/kward/cli/tool_summaries.rb +6 -0
- data/lib/kward/cli.rb +150 -26
- data/lib/kward/clipboard.rb +2 -3
- data/lib/kward/compactor.rb +7 -19
- data/lib/kward/config_files.rb +145 -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 +559 -0
- data/lib/kward/image_attachments.rb +3 -1
- data/lib/kward/interactive_pty_runner.rb +151 -0
- data/lib/kward/local_command_runner.rb +155 -0
- data/lib/kward/local_pty_command_runner.rb +171 -0
- data/lib/kward/model/context_usage.rb +2 -2
- data/lib/kward/model/payloads.rb +2 -5
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +84 -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 +510 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1218 -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 +354 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
- data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +244 -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 +1271 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -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 +288 -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 +451 -57
- 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 +99 -56
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +19 -3
- data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
- data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
- 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 +366 -222
- data/lib/kward/prompts/commands.rb +9 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/memory_methods.rb +83 -0
- data/lib/kward/rpc/server.rb +169 -83
- data/lib/kward/rpc/session_manager.rb +45 -121
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/rpc/tool_metadata.rb +11 -0
- data/lib/kward/rpc/transcript_normalizer.rb +4 -39
- data/lib/kward/scratchpad_runner.rb +56 -0
- data/lib/kward/session_diff.rb +20 -3
- data/lib/kward/session_naming.rb +11 -0
- 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/terminal_keys.rb +84 -0
- data/lib/kward/terminal_sequences.rb +42 -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 +204 -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 +93 -0
- data/lib/kward/workers/job.rb +99 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/queue_runner.rb +166 -0
- data/lib/kward/workers/queue_store.rb +112 -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 +10 -0
- data/lib/kward/workspace.rb +125 -87
- data/templates/default/fulldoc/html/css/kward.css +140 -36
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -0
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +23 -34
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +67 -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
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
require_relative "workers/git_guard"
|
|
2
|
+
require_relative "workers/job"
|
|
3
|
+
require_relative "workers/live_view"
|
|
4
|
+
require_relative "workers/manager"
|
|
5
|
+
require_relative "workers/queue_runner"
|
|
6
|
+
require_relative "workers/queue_store"
|
|
7
|
+
require_relative "workers/store"
|
|
8
|
+
require_relative "workers/tool_policy"
|
|
9
|
+
require_relative "workers/write_lock"
|
|
10
|
+
require_relative "workers/worker"
|
data/lib/kward/workspace.rb
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
require "open3"
|
|
2
1
|
require "pathname"
|
|
3
|
-
|
|
2
|
+
require_relative "local_command_runner"
|
|
4
3
|
require_relative "session_diff"
|
|
5
4
|
|
|
6
5
|
# Namespace for the Kward CLI agent runtime.
|
|
@@ -23,6 +22,7 @@ module Kward
|
|
|
23
22
|
MAX_COMMAND_OUTPUT_BYTES = 128 * 1024
|
|
24
23
|
MAX_EDIT_DIFF_BYTES = 8 * 1024
|
|
25
24
|
DEFAULT_COMMAND_TIMEOUT_SECONDS = 30
|
|
25
|
+
EXPECTED_FILE_ERRORS = [SecurityError, Errno::ENOENT, Errno::EACCES, Errno::EPERM, Errno::EISDIR, Errno::ENOTDIR].freeze
|
|
26
26
|
|
|
27
27
|
# Creates an object for workspace filesystem and shell operations.
|
|
28
28
|
def initialize(root: Dir.pwd, max_file_bytes: MAX_FILE_BYTES, max_read_output_bytes: MAX_READ_OUTPUT_BYTES, max_read_output_lines: MAX_READ_OUTPUT_LINES, max_command_output_bytes: MAX_COMMAND_OUTPUT_BYTES, guardrails: true)
|
|
@@ -45,7 +45,7 @@ module Kward
|
|
|
45
45
|
Dir.children(resolved).sort.map do |entry|
|
|
46
46
|
File.directory?(File.join(resolved, entry)) ? "#{entry}/" : entry
|
|
47
47
|
end.join("\n")
|
|
48
|
-
rescue
|
|
48
|
+
rescue *EXPECTED_FILE_ERRORS => e
|
|
49
49
|
"Error: #{e.message}"
|
|
50
50
|
end
|
|
51
51
|
|
|
@@ -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,8 +64,25 @@ module Kward
|
|
|
64
64
|
content = File.read(resolved)
|
|
65
65
|
return "Error: not a text file: #{path}" if binary_content?(content)
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
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 *EXPECTED_FILE_ERRORS => e
|
|
69
86
|
"Error: #{e.message}"
|
|
70
87
|
end
|
|
71
88
|
|
|
@@ -80,12 +97,8 @@ 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
|
-
|
|
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")
|
|
88
|
-
rescue SecurityError, Errno::ENOENT => e
|
|
100
|
+
file_structure_summary(path, content)
|
|
101
|
+
rescue *EXPECTED_FILE_ERRORS => e
|
|
89
102
|
"Error: #{e.message}"
|
|
90
103
|
end
|
|
91
104
|
|
|
@@ -107,7 +120,7 @@ module Kward
|
|
|
107
120
|
output = "Wrote #{content.bytesize} bytes to #{path}"
|
|
108
121
|
output << "\n#{truncated_diff(path, old_content, content)}" if old_content && old_content != content
|
|
109
122
|
output
|
|
110
|
-
rescue
|
|
123
|
+
rescue *EXPECTED_FILE_ERRORS => e
|
|
111
124
|
"Error: #{e.message}"
|
|
112
125
|
end
|
|
113
126
|
|
|
@@ -130,7 +143,7 @@ module Kward
|
|
|
130
143
|
|
|
131
144
|
File.write(resolved, result[:content])
|
|
132
145
|
"Edited #{path}: replaced #{result[:count]} block(s)\n#{truncated_diff(path, content, result[:content])}"
|
|
133
|
-
rescue
|
|
146
|
+
rescue *EXPECTED_FILE_ERRORS => e
|
|
134
147
|
"Error: #{e.message}"
|
|
135
148
|
end
|
|
136
149
|
|
|
@@ -147,24 +160,14 @@ module Kward
|
|
|
147
160
|
timeout_seconds = DEFAULT_COMMAND_TIMEOUT_SECONDS if timeout_seconds <= 0
|
|
148
161
|
cancellation&.raise_if_cancelled!
|
|
149
162
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
output << "\nSTDOUT:\n#{stdout_reader.value}" unless stdout_reader.value.empty?
|
|
159
|
-
output << "\nSTDERR:\n#{stderr_reader.value}" unless stderr_reader.value.empty?
|
|
160
|
-
truncate_output(output)
|
|
161
|
-
rescue Timeout::Error
|
|
162
|
-
terminate_process(wait_thread.pid)
|
|
163
|
-
"Error: command timed out after #{timeout_seconds} seconds"
|
|
164
|
-
ensure
|
|
165
|
-
stdout_reader&.kill if stdout_reader&.alive?
|
|
166
|
-
stderr_reader&.kill if stderr_reader&.alive?
|
|
167
|
-
end
|
|
163
|
+
result = LocalCommandRunner.new(timeout_seconds: timeout_seconds, max_output_bytes: @max_command_output_bytes).run(command, cwd: @root.to_s, cancellation: cancellation)
|
|
164
|
+
return "Error: command timed out after #{timeout_seconds} seconds" if result.timed_out
|
|
165
|
+
|
|
166
|
+
output = +"Exit status: #{result.exit_status}\n"
|
|
167
|
+
output << "\nSTDOUT:\n#{result.stdout}" unless result.stdout.empty?
|
|
168
|
+
output << "\nSTDERR:\n#{result.stderr}" unless result.stderr.empty?
|
|
169
|
+
output << "\n... truncated to #{@max_command_output_bytes} bytes" if result.truncated
|
|
170
|
+
truncate_output(output)
|
|
168
171
|
rescue Errno::ENOENT, ArgumentError => e
|
|
169
172
|
"Error: #{e.message}"
|
|
170
173
|
end
|
|
@@ -235,24 +238,96 @@ module Kward
|
|
|
235
238
|
"First #{preview_limit} lines:",
|
|
236
239
|
preview,
|
|
237
240
|
"",
|
|
238
|
-
"[Use read_file with offset=#{preview_limit + 1} and limit to continue
|
|
241
|
+
"[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
242
|
].join("\n")
|
|
240
243
|
end
|
|
241
244
|
|
|
245
|
+
def file_structure_summary(path, content)
|
|
246
|
+
lines = content.split("\n", -1)
|
|
247
|
+
outline = source_outline(lines)
|
|
248
|
+
return "No recognizable source structure found in #{path}." if outline.empty?
|
|
249
|
+
|
|
250
|
+
(["# File structure: #{path}", "- Lines: #{lines.length}", "- Bytes: #{content.bytesize}", "", "## Outline"] + outline).join("\n")
|
|
251
|
+
end
|
|
252
|
+
|
|
242
253
|
def source_outline(lines)
|
|
243
|
-
|
|
254
|
+
entries = source_outline_entries(lines)
|
|
255
|
+
entries.first(80).map do |entry|
|
|
256
|
+
range = entry[:end_line] && entry[:end_line] != entry[:line] ? " (range #{entry[:line]}-#{entry[:end_line]}, #{entry[:kind]})" : " (#{entry[:kind]})"
|
|
257
|
+
"line #{entry[:line]}: #{' ' * [entry[:indent] / 2, 6].min}#{entry[:signature]}#{range}"
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def source_outline_entries(lines)
|
|
262
|
+
candidates = []
|
|
244
263
|
lines.each_with_index do |line, index|
|
|
245
|
-
|
|
246
|
-
next unless
|
|
264
|
+
declaration = source_declaration(line.strip)
|
|
265
|
+
next unless declaration
|
|
247
266
|
|
|
248
|
-
|
|
249
|
-
outline << "line #{index + 1}: #{' ' * [indent / 2, 6].min}#{stripped}"
|
|
250
|
-
break if outline.length >= 80
|
|
267
|
+
candidates << declaration.merge(line: index + 1, indent: line[/\A\s*/].to_s.length)
|
|
251
268
|
end
|
|
252
|
-
|
|
269
|
+
candidates.each_with_index do |entry, index|
|
|
270
|
+
following = candidates[(index + 1)..]&.find { |candidate| candidate[:indent] <= entry[:indent] }
|
|
271
|
+
entry[:end_line] = following ? following[:line] - 1 : last_content_line(lines)
|
|
272
|
+
end
|
|
273
|
+
candidates
|
|
253
274
|
end
|
|
254
275
|
|
|
255
|
-
def
|
|
276
|
+
def source_declaration(stripped)
|
|
277
|
+
case stripped
|
|
278
|
+
when /\A(module)\s+(.+)/
|
|
279
|
+
{ kind: "module", signature: stripped }
|
|
280
|
+
when /\A(class)\s+(.+)/
|
|
281
|
+
{ kind: "class", signature: stripped }
|
|
282
|
+
when /\A(async\s+def|def)\s+(.+)/
|
|
283
|
+
{ kind: "function", signature: stripped }
|
|
284
|
+
when /\A(export\s+)?(async\s+)?function\s+(.+)/
|
|
285
|
+
{ kind: "function", signature: stripped }
|
|
286
|
+
when /\A(async\s+)?(?:get\s+|set\s+)?(?:constructor|[A-Za-z_$][\w$]*)\s*\([^;]*\)\s*(?::\s*[^{}]+)?\s*(?:\{\}|\{|=>)?\z/
|
|
287
|
+
{ kind: "method", signature: stripped } unless stripped.match?(/\A(if|for|while|switch|catch)\b/)
|
|
288
|
+
when /\A(export\s+)?(class|interface|type|enum)\s+(.+)/
|
|
289
|
+
{ kind: Regexp.last_match(2), signature: stripped }
|
|
290
|
+
when /\A(?:export\s+)?(?:const|let|var)\s+\w+\s*=.*=>/
|
|
291
|
+
{ kind: "function", signature: stripped }
|
|
292
|
+
when /\Afunc\s+(.+)/
|
|
293
|
+
{ kind: "function", signature: stripped }
|
|
294
|
+
when /\Atype\s+\w+\s+(struct|interface)\b/
|
|
295
|
+
{ kind: Regexp.last_match(1), signature: stripped }
|
|
296
|
+
when /\A(pub\s+)?(async\s+)?fn\s+(.+)/
|
|
297
|
+
{ kind: "function", signature: stripped }
|
|
298
|
+
when /\A(pub\s+)?(struct|enum|trait|impl)\b(.+)?/
|
|
299
|
+
{ kind: Regexp.last_match(2), signature: stripped }
|
|
300
|
+
when /\A(?:public|private|protected|internal|static|final|abstract|async|override|virtual|sealed|readonly|partial|\s)+\s*(class|interface|enum|record)\s+(.+)/
|
|
301
|
+
{ kind: Regexp.last_match(1), signature: stripped }
|
|
302
|
+
when /\A(?:public|private|protected|internal|static|final|abstract|async|override|virtual|sealed|readonly|partial|\s)+\s*\S[^{;=]*\w+\s*\([^;]*\)\s*(?:\{|=>)?\z/
|
|
303
|
+
{ kind: "method", signature: stripped }
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def last_content_line(lines)
|
|
308
|
+
index = lines.rindex { |line| !line.strip.empty? }
|
|
309
|
+
index ? index + 1 : lines.length
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def normalize_read_mode(mode)
|
|
313
|
+
return nil if mode.nil? || mode.to_s.empty?
|
|
314
|
+
|
|
315
|
+
value = mode.to_s.downcase
|
|
316
|
+
return value.to_sym if %w[preview outline range full].include?(value)
|
|
317
|
+
|
|
318
|
+
"Error: mode must be one of preview, outline, range, full"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def read_output_budget(max_bytes)
|
|
322
|
+
return @max_read_output_bytes if max_bytes.nil?
|
|
323
|
+
|
|
324
|
+
value = max_bytes.to_i
|
|
325
|
+
return "Error: max_bytes must be positive" unless value.positive?
|
|
326
|
+
|
|
327
|
+
[value, @max_read_output_bytes].min
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def read_file_slice(content, offset:, limit:, max_bytes: @max_read_output_bytes)
|
|
256
331
|
lines = content.split("\n", -1)
|
|
257
332
|
lines = [""] if lines.empty?
|
|
258
333
|
start_index = read_start_index(offset)
|
|
@@ -263,7 +338,7 @@ module Kward
|
|
|
263
338
|
|
|
264
339
|
selected_end = user_limit ? [start_index + user_limit, lines.length].min : lines.length
|
|
265
340
|
selected_lines = lines[start_index...selected_end]
|
|
266
|
-
truncated = truncate_read_lines(selected_lines)
|
|
341
|
+
truncated = truncate_read_lines(selected_lines, max_bytes: max_bytes)
|
|
267
342
|
return truncated[:error] if truncated[:error]
|
|
268
343
|
|
|
269
344
|
output = truncated[:content]
|
|
@@ -272,7 +347,8 @@ module Kward
|
|
|
272
347
|
start_index: start_index,
|
|
273
348
|
output_lines: truncated[:line_count],
|
|
274
349
|
total_lines: lines.length,
|
|
275
|
-
truncated_by: truncated[:truncated_by]
|
|
350
|
+
truncated_by: truncated[:truncated_by],
|
|
351
|
+
max_bytes: max_bytes
|
|
276
352
|
)
|
|
277
353
|
elsif user_limit && selected_end < lines.length
|
|
278
354
|
output << "\n\n[#{lines.length - selected_end} more lines in file. Use offset=#{selected_end + 1} to continue.]"
|
|
@@ -300,11 +376,11 @@ module Kward
|
|
|
300
376
|
value
|
|
301
377
|
end
|
|
302
378
|
|
|
303
|
-
def truncate_read_lines(lines)
|
|
379
|
+
def truncate_read_lines(lines, max_bytes: @max_read_output_bytes)
|
|
304
380
|
first_line = lines.first.to_s
|
|
305
|
-
if first_line.bytesize >
|
|
381
|
+
if first_line.bytesize > max_bytes
|
|
306
382
|
return {
|
|
307
|
-
error: "Error: first line is #{first_line.bytesize} bytes, exceeds #{
|
|
383
|
+
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
384
|
}
|
|
309
385
|
end
|
|
310
386
|
|
|
@@ -319,7 +395,7 @@ module Kward
|
|
|
319
395
|
|
|
320
396
|
separator_bytes = output_lines.empty? ? 0 : 1
|
|
321
397
|
next_bytes = line.bytesize + separator_bytes
|
|
322
|
-
if bytes + next_bytes >
|
|
398
|
+
if bytes + next_bytes > max_bytes
|
|
323
399
|
truncated_by = "bytes"
|
|
324
400
|
break
|
|
325
401
|
end
|
|
@@ -336,10 +412,10 @@ module Kward
|
|
|
336
412
|
}
|
|
337
413
|
end
|
|
338
414
|
|
|
339
|
-
def read_truncation_notice(start_index:, output_lines:, total_lines:, truncated_by:)
|
|
415
|
+
def read_truncation_notice(start_index:, output_lines:, total_lines:, truncated_by:, max_bytes: @max_read_output_bytes)
|
|
340
416
|
end_line = start_index + output_lines
|
|
341
417
|
next_offset = end_line + 1
|
|
342
|
-
detail = truncated_by == "lines" ? "#{@max_read_output_lines} line limit" : "#{
|
|
418
|
+
detail = truncated_by == "lines" ? "#{@max_read_output_lines} line limit" : "#{max_bytes} byte limit"
|
|
343
419
|
"\n\n[Showing lines #{start_index + 1}-#{end_line} of #{total_lines} (#{detail}). Use offset=#{next_offset} to continue.]"
|
|
344
420
|
end
|
|
345
421
|
|
|
@@ -436,43 +512,5 @@ module Kward
|
|
|
436
512
|
output.byteslice(0, @max_command_output_bytes) << "\n... truncated to #{@max_command_output_bytes} bytes"
|
|
437
513
|
end
|
|
438
514
|
|
|
439
|
-
def wait_for_process(wait_thread, timeout_seconds, cancellation)
|
|
440
|
-
deadline = Time.now + timeout_seconds
|
|
441
|
-
loop do
|
|
442
|
-
cancellation&.raise_if_cancelled!
|
|
443
|
-
if wait_thread.join(0.05)
|
|
444
|
-
cancellation&.raise_if_cancelled!
|
|
445
|
-
return wait_thread.value
|
|
446
|
-
end
|
|
447
|
-
raise Timeout::Error if Time.now >= deadline
|
|
448
|
-
end
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
def terminate_process(pid)
|
|
452
|
-
return unless signal_process("TERM", pid)
|
|
453
|
-
|
|
454
|
-
deadline = Time.now + 0.2
|
|
455
|
-
while Time.now < deadline
|
|
456
|
-
return unless process_running?(pid)
|
|
457
|
-
|
|
458
|
-
sleep 0.02
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
signal_process("KILL", pid)
|
|
462
|
-
end
|
|
463
|
-
|
|
464
|
-
def process_running?(pid)
|
|
465
|
-
Process.kill(0, pid)
|
|
466
|
-
true
|
|
467
|
-
rescue Errno::ESRCH
|
|
468
|
-
false
|
|
469
|
-
end
|
|
470
|
-
|
|
471
|
-
def signal_process(signal, pid)
|
|
472
|
-
Process.kill(signal, pid)
|
|
473
|
-
true
|
|
474
|
-
rescue Errno::ESRCH
|
|
475
|
-
false
|
|
476
|
-
end
|
|
477
515
|
end
|
|
478
516
|
end
|