kward 0.82.0 → 0.84.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 +96 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +1 -0
- data/doc/api.md +4 -0
- data/doc/composer.md +2 -2
- data/doc/configuration.md +82 -20
- data/doc/editor.md +47 -22
- data/doc/files.md +12 -7
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/permissions.md +1 -0
- data/doc/platform-support.md +48 -0
- data/doc/releasing.md +12 -4
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +4 -3
- data/doc/shell.md +101 -60
- data/doc/tabs.md +3 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +7 -6
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +7 -3
- data/lib/kward/ansi.rb +110 -10
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +144 -21
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +27 -0
- data/lib/kward/cli/rendering.rb +15 -9
- data/lib/kward/cli/runtime_helpers.rb +260 -55
- data/lib/kward/cli/slash_commands.rb +21 -12
- data/lib/kward/cli/tabs.rb +91 -13
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +55 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +94 -63
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +103 -24
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +35 -39
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +23 -3
- data/lib/kward/persistent_shell_session.rb +843 -0
- data/lib/kward/project_files.rb +19 -6
- data/lib/kward/prompt_interface/composer_renderer.rb +45 -41
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +27 -22
- data/lib/kward/prompt_interface/editor/controller.rb +99 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +298 -47
- data/lib/kward/prompt_interface/editor/renderer.rb +109 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/state.rb +21 -10
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +92 -22
- data/lib/kward/prompt_interface/key_handler.rb +75 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +443 -17
- data/lib/kward/prompt_interface/runtime_state.rb +55 -2
- data/lib/kward/prompt_interface/screen.rb +2 -2
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +169 -18
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/scratchpad_languages.rb +74 -0
- data/lib/kward/scratchpad_runner.rb +155 -29
- data/lib/kward/shell_prompt.rb +52 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_keys.rb +13 -0
- data/lib/kward/terminal_text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +75 -8
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/version.rb +1 -1
- data/templates/default/fulldoc/html/css/kward.css +0 -125
- data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +6 -18
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +44 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Interactive terminal UI used by the CLI frontend.
|
|
4
|
+
class PromptInterface
|
|
5
|
+
# Current-buffer word completion for the built-in composer file editor.
|
|
6
|
+
module EditorWordCompletion
|
|
7
|
+
EDITOR_COMPLETION_WORD_PATTERN = /[[:alnum:]_]+/.freeze
|
|
8
|
+
EDITOR_COMPLETION_PREFIX_PATTERN = /[[:alnum:]_]+\z/.freeze
|
|
9
|
+
EDITOR_COMPLETION_SUFFIX_PATTERN = /\A[[:alnum:]_]*/.freeze
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def editor_complete_buffer_word
|
|
14
|
+
if (cycle = matching_editor_completion_cycle)
|
|
15
|
+
return cycle_editor_word_completion(cycle) if cycle[:candidates].length > 1
|
|
16
|
+
|
|
17
|
+
reset_editor_word_completion
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context = editor_word_completion_context
|
|
21
|
+
return false unless context
|
|
22
|
+
|
|
23
|
+
candidates = editor_word_completion_candidates(context)
|
|
24
|
+
return false if candidates.empty?
|
|
25
|
+
|
|
26
|
+
@editor_word_completion = context.merge(
|
|
27
|
+
editor_state: @editor_state,
|
|
28
|
+
candidates: candidates,
|
|
29
|
+
index: 0,
|
|
30
|
+
previous_status: @editor_state.status
|
|
31
|
+
)
|
|
32
|
+
apply_editor_word_completion(@editor_word_completion)
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def editor_word_completion_context
|
|
37
|
+
buffer = @editor_state.buffer
|
|
38
|
+
cursor = @editor_state.cursor
|
|
39
|
+
prefix_match = buffer[0...cursor].to_s.match(EDITOR_COMPLETION_PREFIX_PATTERN)
|
|
40
|
+
return nil unless prefix_match
|
|
41
|
+
|
|
42
|
+
suffix = buffer[cursor..].to_s.match(EDITOR_COMPLETION_SUFFIX_PATTERN).to_s
|
|
43
|
+
{
|
|
44
|
+
prefix: prefix_match[0],
|
|
45
|
+
range_start: prefix_match.begin(0),
|
|
46
|
+
range_end: cursor + suffix.length
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def editor_word_completion_candidates(context)
|
|
51
|
+
matches = []
|
|
52
|
+
@editor_state.buffer.to_enum(:scan, EDITOR_COMPLETION_WORD_PATTERN).each do
|
|
53
|
+
match = Regexp.last_match
|
|
54
|
+
next if editor_completion_match_overlaps_context?(match, context)
|
|
55
|
+
|
|
56
|
+
word = match[0]
|
|
57
|
+
next unless word.length > context[:prefix].length
|
|
58
|
+
next unless word.start_with?(context[:prefix])
|
|
59
|
+
|
|
60
|
+
matches << [word, match.begin(0)]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
matches
|
|
64
|
+
.sort_by { |_word, offset| editor_completion_candidate_order(offset, context) }
|
|
65
|
+
.map(&:first)
|
|
66
|
+
.uniq
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def editor_completion_match_overlaps_context?(match, context)
|
|
70
|
+
match.begin(0) < context[:range_end] && match.end(0) > context[:range_start]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def editor_completion_candidate_order(offset, context)
|
|
74
|
+
if offset < context[:range_start]
|
|
75
|
+
[0, context[:range_start] - offset]
|
|
76
|
+
else
|
|
77
|
+
[1, offset - context[:range_end]]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def matching_editor_completion_cycle
|
|
82
|
+
cycle = @editor_word_completion
|
|
83
|
+
return nil unless cycle
|
|
84
|
+
|
|
85
|
+
same_editor = cycle[:editor_state].equal?(@editor_state)
|
|
86
|
+
same_buffer = cycle[:buffer] == @editor_state.buffer
|
|
87
|
+
same_cursor = cycle[:cursor] == @editor_state.cursor
|
|
88
|
+
return cycle if same_editor && same_buffer && same_cursor
|
|
89
|
+
|
|
90
|
+
reset_editor_word_completion
|
|
91
|
+
nil
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def cycle_editor_word_completion(cycle)
|
|
95
|
+
cycle[:index] = (cycle[:index] + 1) % cycle[:candidates].length
|
|
96
|
+
apply_editor_word_completion(cycle)
|
|
97
|
+
true
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def apply_editor_word_completion(cycle)
|
|
101
|
+
candidate = cycle[:candidates][cycle[:index]]
|
|
102
|
+
@editor_state.replace_range(cycle[:range_start], cycle[:range_end], candidate)
|
|
103
|
+
@editor_state.cursor = cycle[:range_start] + candidate.length
|
|
104
|
+
cycle[:range_end] = @editor_state.cursor
|
|
105
|
+
cycle[:buffer] = @editor_state.buffer
|
|
106
|
+
cycle[:cursor] = @editor_state.cursor
|
|
107
|
+
cycle[:status] = "Completion #{cycle[:index] + 1}/#{cycle[:candidates].length}: #{candidate}"
|
|
108
|
+
@editor_state.status = cycle[:status]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def reset_editor_word_completion
|
|
112
|
+
cycle = @editor_word_completion
|
|
113
|
+
if cycle && cycle[:editor_state].status == cycle[:status]
|
|
114
|
+
cycle[:editor_state].status = cycle[:previous_status]
|
|
115
|
+
end
|
|
116
|
+
@editor_word_completion = nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def editor_word_completion_tab_key?(key)
|
|
120
|
+
!editor_tab_sequence_for(key).nil?
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../text_matcher"
|
|
2
|
+
|
|
1
3
|
# Namespace for the Kward CLI agent runtime.
|
|
2
4
|
module Kward
|
|
3
5
|
# File-mention completion overlay behavior.
|
|
@@ -86,36 +88,86 @@ module Kward
|
|
|
86
88
|
def file_overlay_matches
|
|
87
89
|
token = active_file_open || active_file_mention
|
|
88
90
|
return [] unless token
|
|
91
|
+
unless @file_mention_paths
|
|
92
|
+
start_file_mention_discovery_locked
|
|
93
|
+
return []
|
|
94
|
+
end
|
|
89
95
|
|
|
90
96
|
query = token[:query].downcase
|
|
97
|
+
entries = project_file_path_entries
|
|
98
|
+
cache = @file_overlay_match_cache
|
|
99
|
+
return cache[:matches] if cache && cache[:query] == query && cache[:entries].equal?(entries)
|
|
100
|
+
|
|
101
|
+
pattern = TextMatcher.subsequence_pattern(query)
|
|
91
102
|
matches = []
|
|
92
|
-
|
|
93
|
-
next unless
|
|
103
|
+
entries.each do |entry|
|
|
104
|
+
next unless TextMatcher.subsequence?(entry[:downcase], query, pattern)
|
|
94
105
|
|
|
95
106
|
matches << entry[:path]
|
|
96
107
|
break if matches.length >= FILE_MENTION_RESULT_LIMIT
|
|
97
108
|
end
|
|
109
|
+
@file_overlay_match_cache = { query: query, entries: entries, matches: matches }
|
|
98
110
|
matches
|
|
99
111
|
end
|
|
100
112
|
|
|
101
|
-
def
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
def start_file_mention_discovery_locked
|
|
114
|
+
root = prompt_workspace_root
|
|
115
|
+
active_thread = @file_mention_discovery_thread
|
|
116
|
+
return if active_thread&.alive? && @file_mention_discovery_root == root
|
|
117
|
+
|
|
118
|
+
token = Object.new
|
|
119
|
+
start = Queue.new
|
|
120
|
+
@file_mention_discovery_root = root
|
|
121
|
+
@file_mention_discovery_token = token
|
|
122
|
+
worker = Thread.new do
|
|
123
|
+
start.pop
|
|
124
|
+
paths = discover_file_mention_paths(root)
|
|
125
|
+
entries = build_file_path_entries(paths)
|
|
126
|
+
@mutex.synchronize do
|
|
127
|
+
next unless @file_mention_discovery_token.equal?(token)
|
|
128
|
+
|
|
129
|
+
@file_mention_discovery_thread = nil
|
|
130
|
+
@file_mention_discovery_root = nil
|
|
131
|
+
@file_mention_discovery_token = nil
|
|
132
|
+
next unless root == prompt_workspace_root
|
|
133
|
+
|
|
134
|
+
@file_mention_paths = paths
|
|
135
|
+
@file_mention_path_entries_paths = paths
|
|
136
|
+
@file_mention_path_entries = entries
|
|
137
|
+
@file_overlay_match_cache = nil
|
|
138
|
+
render_prompt_locked if @started && @asking && file_overlay_visible?
|
|
113
139
|
end
|
|
114
140
|
end
|
|
141
|
+
worker.report_on_exception = false
|
|
142
|
+
@file_mention_discovery_thread = worker
|
|
143
|
+
start << true
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def discover_file_mention_paths(root)
|
|
147
|
+
ProjectFiles.list(root: root)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def finish_file_mention_discovery_locked
|
|
151
|
+
root = prompt_workspace_root
|
|
152
|
+
@file_mention_discovery_thread = nil
|
|
153
|
+
@file_mention_discovery_root = nil
|
|
154
|
+
@file_mention_discovery_token = nil
|
|
155
|
+
@file_mention_paths = discover_file_mention_paths(root)
|
|
156
|
+
@file_mention_path_entries_paths = @file_mention_paths
|
|
157
|
+
@file_mention_path_entries = build_file_path_entries(@file_mention_paths)
|
|
158
|
+
@file_overlay_match_cache = nil
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def file_mention_discovery_loading?
|
|
162
|
+
@file_mention_discovery_thread&.alive? && @file_mention_discovery_root == prompt_workspace_root
|
|
115
163
|
end
|
|
116
164
|
|
|
117
|
-
def project_file_paths
|
|
118
|
-
|
|
165
|
+
def project_file_paths(include_ignored: false)
|
|
166
|
+
if include_ignored
|
|
167
|
+
@project_browser_file_paths ||= discover_project_file_paths(include_ignored: true)
|
|
168
|
+
else
|
|
169
|
+
@file_mention_paths ||= discover_project_file_paths
|
|
170
|
+
end
|
|
119
171
|
end
|
|
120
172
|
|
|
121
173
|
def project_file_path_entries
|
|
@@ -123,17 +175,31 @@ module Kward
|
|
|
123
175
|
return @file_mention_path_entries if @file_mention_path_entries_paths.equal?(paths) && @file_mention_path_entries
|
|
124
176
|
|
|
125
177
|
@file_mention_path_entries_paths = paths
|
|
126
|
-
@file_mention_path_entries = paths
|
|
178
|
+
@file_mention_path_entries = build_file_path_entries(paths)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def build_file_path_entries(paths)
|
|
182
|
+
paths.map { |path| { path: path, downcase: path.downcase } }
|
|
127
183
|
end
|
|
128
184
|
|
|
129
|
-
def discover_project_file_paths
|
|
130
|
-
paths =
|
|
131
|
-
|
|
185
|
+
def discover_project_file_paths(include_ignored: false)
|
|
186
|
+
paths = if include_ignored
|
|
187
|
+
git_project_file_paths(include_ignored: true)
|
|
188
|
+
else
|
|
189
|
+
git_project_file_paths
|
|
190
|
+
end
|
|
191
|
+
paths = scanned_project_file_paths if paths.empty? && !git_project_repository?
|
|
132
192
|
paths.reject { |path| path.empty? || path.end_with?("/") }.uniq.sort
|
|
133
193
|
end
|
|
134
194
|
|
|
135
|
-
def git_project_file_paths
|
|
136
|
-
ProjectFiles.git_paths(prompt_workspace_root)
|
|
195
|
+
def git_project_file_paths(include_ignored: false)
|
|
196
|
+
return ProjectFiles.git_paths(prompt_workspace_root) unless include_ignored
|
|
197
|
+
|
|
198
|
+
ProjectFiles.git_paths(prompt_workspace_root, include_ignored: true)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def git_project_repository?
|
|
202
|
+
ProjectFiles.git_repository?(prompt_workspace_root)
|
|
137
203
|
end
|
|
138
204
|
|
|
139
205
|
def scanned_project_file_paths
|
|
@@ -151,6 +217,7 @@ module Kward
|
|
|
151
217
|
def selected_file_overlay_path
|
|
152
218
|
return nil unless file_overlay_visible?
|
|
153
219
|
|
|
220
|
+
finish_file_mention_discovery_locked unless @file_mention_paths
|
|
154
221
|
matches = file_overlay_matches
|
|
155
222
|
return nil if matches.empty?
|
|
156
223
|
|
|
@@ -186,6 +253,9 @@ module Kward
|
|
|
186
253
|
return [] unless file_overlay_visible?
|
|
187
254
|
|
|
188
255
|
matches = file_overlay_matches
|
|
256
|
+
if file_mention_discovery_loading?
|
|
257
|
+
return overlay_card_rows("Files", [overlay_text_line("Loading project files…", :muted)], width)
|
|
258
|
+
end
|
|
189
259
|
if matches.empty?
|
|
190
260
|
return overlay_card_rows("Files", [overlay_text_line("No matching files", :muted)], width)
|
|
191
261
|
end
|
|
@@ -6,6 +6,73 @@ module Kward
|
|
|
6
6
|
module KeyHandler
|
|
7
7
|
private
|
|
8
8
|
|
|
9
|
+
def filter_terminal_handoff_input_locked(chunk)
|
|
10
|
+
@handoff_input_buffer << chunk.to_s.b
|
|
11
|
+
forwarded = +"".b
|
|
12
|
+
tab_action = nil
|
|
13
|
+
|
|
14
|
+
until @handoff_input_buffer.empty?
|
|
15
|
+
if @handoff_input_buffer.start_with?(TerminalKeys::CTRL_T)
|
|
16
|
+
action = handle_tab_key_binding(TerminalKeys::CTRL_T)
|
|
17
|
+
if action != false
|
|
18
|
+
remainder = @handoff_input_buffer.byteslice(TerminalKeys::CTRL_T.bytesize..).to_s
|
|
19
|
+
queue_pending_keys(remainder) unless remainder.empty?
|
|
20
|
+
@handoff_input_buffer.clear
|
|
21
|
+
tab_action = action
|
|
22
|
+
break
|
|
23
|
+
end
|
|
24
|
+
forwarded << @handoff_input_buffer.slice!(0)
|
|
25
|
+
next
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if @handoff_input_buffer.start_with?("\e")
|
|
29
|
+
complete = TerminalKeys::TAB_ACTION_SEQUENCES.find { |sequence| @handoff_input_buffer.start_with?(sequence) }
|
|
30
|
+
partial = TerminalKeys::TAB_ACTION_SEQUENCES.any? { |sequence| sequence.start_with?(@handoff_input_buffer) }
|
|
31
|
+
break if partial && !complete
|
|
32
|
+
|
|
33
|
+
if complete
|
|
34
|
+
action = handle_tab_key_binding(complete)
|
|
35
|
+
if action != false
|
|
36
|
+
remainder = @handoff_input_buffer.byteslice(complete.bytesize..).to_s
|
|
37
|
+
queue_pending_keys(remainder) unless remainder.empty?
|
|
38
|
+
@handoff_input_buffer.clear
|
|
39
|
+
tab_action = action
|
|
40
|
+
break
|
|
41
|
+
end
|
|
42
|
+
forwarded << complete
|
|
43
|
+
@handoff_input_buffer = @handoff_input_buffer.byteslice(complete.bytesize..).to_s.b
|
|
44
|
+
next
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
if (sequence = parse_csi_u_key(@handoff_input_buffer)) && @handoff_input_buffer.start_with?(sequence[:sequence])
|
|
48
|
+
legacy_key = legacy_handoff_key(sequence)
|
|
49
|
+
unless legacy_key.nil?
|
|
50
|
+
forwarded << legacy_key
|
|
51
|
+
@handoff_input_buffer = @handoff_input_buffer.byteslice(sequence[:sequence].bytesize..).to_s.b
|
|
52
|
+
next
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
forwarded << @handoff_input_buffer.slice!(0)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
{ input: forwarded, tab_action: tab_action }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def legacy_handoff_key(sequence)
|
|
64
|
+
event = csi_u_key_event(sequence)
|
|
65
|
+
return event[:text] if event[:type] == :printable
|
|
66
|
+
return "\r" if event[:type] == :enter
|
|
67
|
+
return "\b" if event[:type] == :backspace
|
|
68
|
+
return "\t" if event[:type] == :tab
|
|
69
|
+
|
|
70
|
+
return unless event[:type] == :modified && ctrl_modifier?(event[:modifier])
|
|
71
|
+
return unless event[:code].between?(32, 126)
|
|
72
|
+
|
|
73
|
+
(event[:code] & 0x1f).chr
|
|
74
|
+
end
|
|
75
|
+
|
|
9
76
|
def read_key(nonblock: false)
|
|
10
77
|
pending = @pending_keys.shift unless @pending_keys.empty?
|
|
11
78
|
return pending if pending
|
|
@@ -20,6 +87,14 @@ module Kward
|
|
|
20
87
|
end
|
|
21
88
|
|
|
22
89
|
def handle_editor_input_key(key)
|
|
90
|
+
if @editor_agent_suspended
|
|
91
|
+
return CANCEL_INPUT if @busy && key == TerminalKeys::CTRL_C
|
|
92
|
+
|
|
93
|
+
return true
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
reset_editor_word_completion unless editor_word_completion_tab_key?(key)
|
|
97
|
+
|
|
23
98
|
tab_result = handle_tab_key_binding(key)
|
|
24
99
|
return tab_result unless tab_result == false
|
|
25
100
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require_relative "../terminal_text"
|
|
2
|
+
|
|
1
3
|
# Namespace for the Kward CLI agent runtime.
|
|
2
4
|
module Kward
|
|
3
5
|
# Shared overlay drawing helpers for prompt UI popups.
|
|
@@ -63,7 +65,7 @@ module Kward
|
|
|
63
65
|
|
|
64
66
|
def overlay_top_border(title, card_width)
|
|
65
67
|
title = visible_truncate(title.to_s, [card_width - 4, 1].max)
|
|
66
|
-
plain_length = ANSI.strip(title)
|
|
68
|
+
plain_length = TerminalText.width(ANSI.strip(title))
|
|
67
69
|
colored("╭", :primary_green) + " #{colored(title, :primary_green, :bold)} " + colored("─" * [card_width - plain_length - 4, 0].max, :primary_green) + colored("╮", :primary_green)
|
|
68
70
|
end
|
|
69
71
|
|
|
@@ -98,7 +100,7 @@ module Kward
|
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
def align_overlay_row(row, width)
|
|
101
|
-
plain_length = ANSI.strip(row)
|
|
103
|
+
plain_length = TerminalText.width(ANSI.strip(row))
|
|
102
104
|
padding = [width - plain_length, 0].max
|
|
103
105
|
left = overlay_left_padding(width, plain_length)
|
|
104
106
|
right = padding - left
|
|
@@ -152,14 +154,14 @@ module Kward
|
|
|
152
154
|
end
|
|
153
155
|
|
|
154
156
|
def visible_ljust(text, width)
|
|
155
|
-
text.to_s + (" " * [width - ANSI.strip(text.to_s)
|
|
157
|
+
text.to_s + (" " * [width - TerminalText.width(ANSI.strip(text.to_s)), 0].max)
|
|
156
158
|
end
|
|
157
159
|
|
|
158
160
|
def visible_truncate(text, width)
|
|
159
161
|
plain = ANSI.strip(text.to_s)
|
|
160
|
-
return text.to_s if plain
|
|
162
|
+
return text.to_s if TerminalText.width(plain) <= width
|
|
161
163
|
|
|
162
|
-
plain
|
|
164
|
+
TerminalText.truncate(plain, width)
|
|
163
165
|
end
|
|
164
166
|
|
|
165
167
|
end
|