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.
Files changed (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +68 -15
  3. data/CONTRIBUTING.md +74 -0
  4. data/Gemfile.lock +8 -2
  5. data/README.md +21 -1
  6. data/Rakefile +46 -2
  7. data/SECURITY.md +31 -0
  8. data/doc/api.md +4 -0
  9. data/doc/composer.md +1 -1
  10. data/doc/configuration.md +68 -21
  11. data/doc/editor.md +28 -13
  12. data/doc/files.md +8 -4
  13. data/doc/getting-started.md +3 -0
  14. data/doc/pan.md +19 -15
  15. data/doc/platform-support.md +48 -0
  16. data/doc/security.md +3 -2
  17. data/doc/shell.md +62 -45
  18. data/doc/troubleshooting.md +12 -2
  19. data/doc/usage.md +5 -5
  20. data/kward.gemspec +5 -4
  21. data/lib/kward/agent.rb +6 -3
  22. data/lib/kward/ansi.rb +110 -10
  23. data/lib/kward/cli/auth_commands.rb +34 -13
  24. data/lib/kward/cli/commands.rb +83 -62
  25. data/lib/kward/cli/doctor.rb +39 -17
  26. data/lib/kward/cli/hook_commands.rb +22 -12
  27. data/lib/kward/cli/interactive_turn.rb +48 -7
  28. data/lib/kward/cli/project_skills_commands.rb +8 -4
  29. data/lib/kward/cli/prompt_interface.rb +27 -0
  30. data/lib/kward/cli/rendering.rb +15 -9
  31. data/lib/kward/cli/runtime_helpers.rb +118 -55
  32. data/lib/kward/cli/slash_commands.rb +12 -14
  33. data/lib/kward/cli/tabs.rb +83 -12
  34. data/lib/kward/cli/tool_summaries.rb +14 -0
  35. data/lib/kward/cli.rb +45 -7
  36. data/lib/kward/cli_transcript_formatter.rb +11 -4
  37. data/lib/kward/config_files.rb +82 -68
  38. data/lib/kward/detached_run.rb +44 -0
  39. data/lib/kward/interactive_pty_runner.rb +102 -28
  40. data/lib/kward/{ekwsh.rb → kwsh.rb} +35 -16
  41. data/lib/kward/kwshrc.rb +233 -0
  42. data/lib/kward/markdown_code_block.rb +136 -0
  43. data/lib/kward/model/client.rb +34 -22
  44. data/lib/kward/model/provider_catalog.rb +5 -0
  45. data/lib/kward/model/stream_parser.rb +20 -4
  46. data/lib/kward/pan/index.html.erb +3 -3
  47. data/lib/kward/pan/server.rb +23 -3
  48. data/lib/kward/persistent_shell_session.rb +119 -26
  49. data/lib/kward/project_files.rb +2 -2
  50. data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
  51. data/lib/kward/prompt_interface/composer_state.rb +33 -24
  52. data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
  53. data/lib/kward/prompt_interface/editor/controller.rb +30 -33
  54. data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
  55. data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
  56. data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
  57. data/lib/kward/prompt_interface/editor/runner.rb +362 -0
  58. data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
  59. data/lib/kward/prompt_interface/editor/state.rb +10 -10
  60. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
  61. data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
  62. data/lib/kward/prompt_interface/file_overlay.rb +71 -15
  63. data/lib/kward/prompt_interface/key_handler.rb +67 -0
  64. data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
  65. data/lib/kward/prompt_interface/project_browser.rb +415 -14
  66. data/lib/kward/prompt_interface/runtime_state.rb +50 -1
  67. data/lib/kward/prompt_interface/screen.rb +2 -2
  68. data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
  69. data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
  70. data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
  71. data/lib/kward/prompt_interface.rb +93 -18
  72. data/lib/kward/prompts/commands.rb +1 -1
  73. data/lib/kward/prompts.rb +1 -1
  74. data/lib/kward/pty_output_sink.rb +47 -0
  75. data/lib/kward/rpc/transcript_normalizer.rb +7 -3
  76. data/lib/kward/scratchpad_languages.rb +74 -0
  77. data/lib/kward/scratchpad_runner.rb +155 -29
  78. data/lib/kward/shell_prompt.rb +2 -0
  79. data/lib/kward/terminal_keys.rb +12 -0
  80. data/lib/kward/terminal_text.rb +121 -0
  81. data/lib/kward/text_matcher.rb +18 -0
  82. data/lib/kward/tools/open_editor.rb +41 -0
  83. data/lib/kward/tools/registry.rb +13 -4
  84. data/lib/kward/tools/tool_call.rb +2 -1
  85. data/lib/kward/version.rb +1 -1
  86. data/templates/default/fulldoc/html/css/kward.css +0 -125
  87. data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
  88. data/templates/default/fulldoc/html/setup.rb +1 -1
  89. data/templates/default/kward_navigation.rb +1 -0
  90. data/templates/default/layout/html/footer.erb +10 -0
  91. data/templates/default/layout/html/headers.erb +23 -0
  92. data/templates/default/layout/html/layout.erb +6 -18
  93. data/templates/default/layout/html/setup.rb +41 -2
  94. metadata +36 -8
  95. 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
- project_file_path_entries.each do |entry|
93
- next unless file_mention_match?(entry[:downcase], query)
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 file_mention_match?(path, query)
102
- return true if query.empty?
103
- return true if path.include?(query)
104
-
105
- query_chars = query.chars
106
- query_chars.all? do |char|
107
- index = path.index(char)
108
- if index
109
- path = path[(index + 1)..].to_s
110
- true
111
- else
112
- false
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.map { |path| { path: path, downcase: path.downcase } }
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).length
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).length
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).length, 0].max)
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.length <= width
162
+ return text.to_s if TerminalText.width(plain) <= width
161
163
 
162
- plain[0, width]
164
+ TerminalText.truncate(plain, width)
163
165
  end
164
166
 
165
167
  end