kward 0.83.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 +68 -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/api.md +4 -0
- data/doc/composer.md +1 -1
- data/doc/configuration.md +68 -21
- data/doc/editor.md +28 -13
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/platform-support.md +48 -0
- data/doc/security.md +3 -2
- data/doc/shell.md +62 -45
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +5 -5
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +6 -3
- data/lib/kward/ansi.rb +110 -10
- 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 +48 -7
- 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 +118 -55
- data/lib/kward/cli/slash_commands.rb +12 -14
- data/lib/kward/cli/tabs.rb +83 -12
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +45 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/config_files.rb +82 -68
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +35 -16
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +34 -22
- 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 +119 -26
- data/lib/kward/project_files.rb +2 -2
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -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 +10 -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/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +50 -1
- 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 +93 -18
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- 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 +2 -0
- data/lib/kward/terminal_keys.rb +12 -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/registry.rb +13 -4
- 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 +36 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
|
@@ -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,32 +88,78 @@ 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
165
|
def project_file_paths(include_ignored: false)
|
|
@@ -127,7 +175,11 @@ module Kward
|
|
|
127
175
|
return @file_mention_path_entries if @file_mention_path_entries_paths.equal?(paths) && @file_mention_path_entries
|
|
128
176
|
|
|
129
177
|
@file_mention_path_entries_paths = paths
|
|
130
|
-
@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 } }
|
|
131
183
|
end
|
|
132
184
|
|
|
133
185
|
def discover_project_file_paths(include_ignored: false)
|
|
@@ -165,6 +217,7 @@ module Kward
|
|
|
165
217
|
def selected_file_overlay_path
|
|
166
218
|
return nil unless file_overlay_visible?
|
|
167
219
|
|
|
220
|
+
finish_file_mention_discovery_locked unless @file_mention_paths
|
|
168
221
|
matches = file_overlay_matches
|
|
169
222
|
return nil if matches.empty?
|
|
170
223
|
|
|
@@ -200,6 +253,9 @@ module Kward
|
|
|
200
253
|
return [] unless file_overlay_visible?
|
|
201
254
|
|
|
202
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
|
|
203
259
|
if matches.empty?
|
|
204
260
|
return overlay_card_rows("Files", [overlay_text_line("No matching files", :muted)], width)
|
|
205
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
|
|
@@ -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
|