kward 0.73.1 → 0.75.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 +51 -0
- data/Gemfile.lock +2 -2
- data/README.md +4 -2
- data/Rakefile +18 -0
- data/doc/configuration.md +70 -4
- data/doc/extensibility.md +6 -19
- data/doc/getting-started.md +1 -1
- data/doc/lifecycle-hooks.md +424 -0
- data/doc/mcp.md +72 -0
- data/doc/memory.md +1 -1
- data/doc/personas.md +2 -2
- data/doc/plugins.md +1 -1
- data/doc/releasing.md +7 -15
- data/doc/rpc.md +108 -15
- data/doc/session-management.md +9 -9
- data/doc/skills.md +199 -0
- data/doc/usage.md +14 -7
- data/lib/kward/agent.rb +72 -22
- data/lib/kward/auth/anthropic_oauth.rb +5 -87
- data/lib/kward/auth/oauth_helpers.rb +105 -0
- data/lib/kward/auth/openai_oauth.rb +4 -86
- data/lib/kward/cli/commands.rb +12 -2
- data/lib/kward/cli/compaction.rb +7 -0
- data/lib/kward/cli/doctor.rb +18 -10
- data/lib/kward/cli/git.rb +30 -3
- data/lib/kward/cli/hook_commands.rb +258 -0
- data/lib/kward/cli/interactive_turn.rb +9 -3
- data/lib/kward/cli/memory_commands.rb +1 -5
- data/lib/kward/cli/plugins.rb +41 -1
- data/lib/kward/cli/prompt_interface.rb +32 -3
- data/lib/kward/cli/rendering.rb +1 -1
- data/lib/kward/cli/runtime_helpers.rb +44 -5
- data/lib/kward/cli/sessions.rb +13 -0
- data/lib/kward/cli/settings.rb +226 -152
- data/lib/kward/cli/slash_commands.rb +81 -42
- data/lib/kward/cli/tabs.rb +32 -15
- data/lib/kward/cli.rb +64 -4
- data/lib/kward/cli_transcript_formatter.rb +0 -7
- data/lib/kward/compaction/token_estimator.rb +112 -0
- data/lib/kward/compactor.rb +42 -116
- data/lib/kward/config_files.rb +152 -27
- data/lib/kward/deep_copy.rb +42 -0
- data/lib/kward/diff_view_mode.rb +36 -0
- data/lib/kward/events.rb +1 -0
- data/lib/kward/export_path.rb +2 -6
- data/lib/kward/frontmatter.rb +43 -0
- data/lib/kward/hooks/audit_log.rb +121 -0
- data/lib/kward/hooks/catalog.rb +110 -0
- data/lib/kward/hooks/command_handler.rb +66 -0
- data/lib/kward/hooks/config_loader.rb +104 -0
- data/lib/kward/hooks/decision.rb +102 -0
- data/lib/kward/hooks/event.rb +60 -0
- data/lib/kward/hooks/http_handler.rb +72 -0
- data/lib/kward/hooks/manager.rb +158 -0
- data/lib/kward/hooks/matcher.rb +75 -0
- data/lib/kward/hooks.rb +16 -0
- data/lib/kward/markdown_transcript.rb +4 -3
- data/lib/kward/mcp/client.rb +56 -0
- data/lib/kward/mcp/server_config.rb +59 -0
- data/lib/kward/mcp/stdio_transport.rb +112 -0
- data/lib/kward/memory/turn_context.rb +19 -0
- data/lib/kward/message_access.rb +2 -2
- data/lib/kward/message_text.rb +19 -0
- data/lib/kward/model/client.rb +13 -45
- data/lib/kward/model/context_usage.rb +10 -4
- data/lib/kward/model/copilot_models.rb +44 -0
- data/lib/kward/model/model_info.rb +24 -0
- data/lib/kward/model/payloads.rb +15 -17
- data/lib/kward/model/stream_parser.rb +178 -13
- data/lib/kward/pan/server.rb +51 -2
- data/lib/kward/path_guard.rb +13 -0
- data/lib/kward/plugin_registry.rb +86 -25
- data/lib/kward/prompt_interface/editor/controller.rb +124 -4
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +10 -4
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +448 -20
- data/lib/kward/prompt_interface/editor/renderer.rb +27 -0
- data/lib/kward/prompt_interface/editor/search.rb +70 -13
- data/lib/kward/prompt_interface/editor/state.rb +72 -13
- data/lib/kward/prompt_interface/editor/vibe_state.rb +12 -1
- data/lib/kward/prompt_interface/selection_prompt.rb +26 -6
- data/lib/kward/prompt_interface.rb +4 -1
- data/lib/kward/prompts/commands.rb +3 -3
- data/lib/kward/prompts.rb +7 -7
- data/lib/kward/rpc/mcp_status.rb +50 -0
- data/lib/kward/rpc/prompt_bridge.rb +38 -4
- data/lib/kward/rpc/runtime_payloads.rb +1 -1
- data/lib/kward/rpc/server.rb +94 -13
- data/lib/kward/rpc/session_manager.rb +258 -33
- data/lib/kward/rpc/session_metrics.rb +1 -1
- data/lib/kward/rpc/session_tree_rows.rb +13 -64
- data/lib/kward/rpc/tool_event_normalizer.rb +16 -2
- data/lib/kward/rpc/transcript_normalizer.rb +9 -10
- data/lib/kward/rpc/turn_context.rb +109 -0
- data/lib/kward/session_store.rb +6 -20
- data/lib/kward/session_tree_nodes.rb +98 -18
- data/lib/kward/session_tree_renderer.rb +6 -61
- data/lib/kward/skills/registry.rb +109 -17
- data/lib/kward/tools/mcp_tool.rb +122 -0
- data/lib/kward/tools/read_skill.rb +9 -4
- data/lib/kward/tools/registry.rb +297 -9
- data/lib/kward/tools/search/code.rb +4 -22
- data/lib/kward/tools/search/web.rb +28 -2
- data/lib/kward/update_check.rb +117 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/manager.rb +43 -3
- data/lib/kward/workspace.rb +3 -1
- data/templates/default/fulldoc/html/css/kward.css +22 -0
- data/templates/default/fulldoc/html/js/kward.js +2 -0
- data/templates/default/kward_navigation.rb +4 -2
- data/templates/default/layout/html/setup.rb +2 -0
- metadata +30 -1
|
@@ -88,10 +88,26 @@ module Kward
|
|
|
88
88
|
visible = line.to_s[column_offset.to_i, text_width].to_s
|
|
89
89
|
rendered = editor_render_visible_line(visible, line_index)
|
|
90
90
|
line_start = @editor_state.line_start_offset(line_index)
|
|
91
|
+
rendered = editor_overlay_search_matches(rendered, line_start, column_offset, visible.length)
|
|
91
92
|
rendered = editor_overlay_line_selections(rendered, line_start, column_offset, visible.length)
|
|
92
93
|
editor_overlay_secondary_cursors(rendered, line_start, column_offset, visible.length, text_width)
|
|
93
94
|
end
|
|
94
95
|
|
|
96
|
+
def editor_overlay_search_matches(rendered, line_start, column_offset, visible_length)
|
|
97
|
+
ranges = @editor_state.search_match_ranges
|
|
98
|
+
return rendered if ranges.empty?
|
|
99
|
+
|
|
100
|
+
visible_offset = line_start + column_offset.to_i
|
|
101
|
+
match_ranges = ranges.filter_map do |range|
|
|
102
|
+
match_start = [range[0] - visible_offset, 0].max
|
|
103
|
+
match_end = [range[1] - visible_offset, visible_length].min
|
|
104
|
+
[match_start, match_end] if match_start < match_end
|
|
105
|
+
end
|
|
106
|
+
return rendered if match_ranges.empty?
|
|
107
|
+
|
|
108
|
+
editor_overlay_selection(rendered, match_ranges)
|
|
109
|
+
end
|
|
110
|
+
|
|
95
111
|
def editor_overlay_line_selections(rendered, line_start, column_offset, visible_length)
|
|
96
112
|
ranges = @editor_state.selection_ranges
|
|
97
113
|
return rendered if ranges.empty?
|
|
@@ -158,6 +174,7 @@ module Kward
|
|
|
158
174
|
|
|
159
175
|
def editor_render_diff_line(line)
|
|
160
176
|
text = line.to_s
|
|
177
|
+
return editor_render_side_by_side_diff_line(text) if @editor_state.side_by_side_diff_view?
|
|
161
178
|
return colored(text, :green) if text.start_with?("+") && !text.start_with?("+++")
|
|
162
179
|
return colored(text, :red) if text.start_with?("-") && !text.start_with?("---")
|
|
163
180
|
return colored(text, :cyan) if text.start_with?("@@")
|
|
@@ -165,6 +182,16 @@ module Kward
|
|
|
165
182
|
text
|
|
166
183
|
end
|
|
167
184
|
|
|
185
|
+
def editor_render_side_by_side_diff_line(text)
|
|
186
|
+
return colored(text, :cyan) if text.start_with?("@@")
|
|
187
|
+
return text unless text.include?(" │ ")
|
|
188
|
+
|
|
189
|
+
left, right = text.split(" │ ", 2)
|
|
190
|
+
rendered_left = left.start_with?("-") ? colored(left, :red) : left
|
|
191
|
+
rendered_right = right.start_with?("+") ? colored(right, :green) : right
|
|
192
|
+
"#{rendered_left} │ #{rendered_right}"
|
|
193
|
+
end
|
|
194
|
+
|
|
168
195
|
def editor_line_number_gutter_width
|
|
169
196
|
[[@editor_state.lines.length.to_s.length, 4].max + 3, 1].max
|
|
170
197
|
end
|
|
@@ -6,44 +6,82 @@ module Kward
|
|
|
6
6
|
class EditorSearch
|
|
7
7
|
attr_reader :query, :direction
|
|
8
8
|
|
|
9
|
+
def self.match_ranges(text, query, base_offset: 0)
|
|
10
|
+
query = query.to_s
|
|
11
|
+
return [] if query.empty?
|
|
12
|
+
|
|
13
|
+
haystack, needle = normalized_pair(text.to_s, query)
|
|
14
|
+
ranges = []
|
|
15
|
+
start = 0
|
|
16
|
+
while (index = haystack.index(needle, start))
|
|
17
|
+
ranges << [base_offset + index, base_offset + index + query.length]
|
|
18
|
+
start = index + [query.length, 1].max
|
|
19
|
+
end
|
|
20
|
+
ranges
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.normalized_pair(buffer, query)
|
|
24
|
+
return [buffer, query] if case_sensitive?(query)
|
|
25
|
+
|
|
26
|
+
[buffer.downcase, query.downcase]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def self.case_sensitive?(query)
|
|
30
|
+
query.to_s.match?(/[[:upper:]]/)
|
|
31
|
+
end
|
|
32
|
+
|
|
9
33
|
def initialize(direction: :forward)
|
|
10
34
|
@active = false
|
|
11
35
|
@query = +""
|
|
12
36
|
@direction = direction
|
|
37
|
+
@origin_cursor = nil
|
|
38
|
+
@current_match = nil
|
|
13
39
|
end
|
|
14
40
|
|
|
15
41
|
def active?
|
|
16
42
|
@active == true
|
|
17
43
|
end
|
|
18
44
|
|
|
19
|
-
def begin(direction = :forward)
|
|
45
|
+
def begin(direction = :forward, cursor: nil)
|
|
20
46
|
@active = true
|
|
21
47
|
@direction = direction
|
|
22
48
|
@query = +""
|
|
49
|
+
@origin_cursor = cursor
|
|
50
|
+
@current_match = nil
|
|
23
51
|
status_prefix
|
|
24
52
|
end
|
|
25
53
|
|
|
26
|
-
def cancel
|
|
54
|
+
def cancel(restore_cursor: true)
|
|
27
55
|
@active = false
|
|
28
|
-
|
|
56
|
+
cursor = @origin_cursor if restore_cursor
|
|
57
|
+
@origin_cursor = nil
|
|
58
|
+
@current_match = nil
|
|
59
|
+
{ cursor: cursor, status: "Search cancelled", found: false }
|
|
29
60
|
end
|
|
30
61
|
|
|
31
|
-
def append(text)
|
|
62
|
+
def append(text, buffer:, cursor:)
|
|
32
63
|
@query << text.to_s
|
|
33
|
-
|
|
64
|
+
live_result(buffer: buffer, cursor: cursor)
|
|
34
65
|
end
|
|
35
66
|
|
|
36
|
-
def delete_character
|
|
67
|
+
def delete_character(buffer:, cursor:)
|
|
37
68
|
@query = @query[0...-1].to_s
|
|
38
|
-
|
|
69
|
+
live_result(buffer: buffer, cursor: cursor)
|
|
39
70
|
end
|
|
40
71
|
|
|
41
72
|
def confirm(buffer:, cursor:)
|
|
42
73
|
confirmed_query = @query.to_s
|
|
43
74
|
@active = false
|
|
75
|
+
@origin_cursor = nil
|
|
44
76
|
return { status: "Search cancelled", found: false } if confirmed_query.empty?
|
|
45
77
|
|
|
78
|
+
if @current_match
|
|
79
|
+
return { cursor: @current_match, status: "Found: #{confirmed_query}", found: true }
|
|
80
|
+
end
|
|
81
|
+
|
|
46
82
|
repeat(buffer: buffer, cursor: cursor, direction: @direction, query: confirmed_query)
|
|
83
|
+
ensure
|
|
84
|
+
@current_match = nil
|
|
47
85
|
end
|
|
48
86
|
|
|
49
87
|
def repeat(buffer:, cursor:, direction: @direction, query: @query)
|
|
@@ -52,12 +90,7 @@ module Kward
|
|
|
52
90
|
|
|
53
91
|
@query = query
|
|
54
92
|
@direction = direction
|
|
55
|
-
index =
|
|
56
|
-
search_from = cursor.positive? ? cursor - 1 : buffer.length
|
|
57
|
-
buffer.rindex(query, search_from) || buffer.rindex(query)
|
|
58
|
-
else
|
|
59
|
-
buffer.index(query, cursor + 1) || buffer.index(query)
|
|
60
|
-
end
|
|
93
|
+
index = find_match(buffer, query, cursor, direction)
|
|
61
94
|
|
|
62
95
|
if index
|
|
63
96
|
{ cursor: index, status: "Found: #{query}", found: true }
|
|
@@ -68,6 +101,30 @@ module Kward
|
|
|
68
101
|
|
|
69
102
|
private
|
|
70
103
|
|
|
104
|
+
def live_result(buffer:, cursor:)
|
|
105
|
+
return { cursor: @origin_cursor, status: status_prefix, found: false } if @query.empty?
|
|
106
|
+
|
|
107
|
+
@origin_cursor = cursor if @origin_cursor.nil?
|
|
108
|
+
index = find_match(buffer, @query, @origin_cursor, @direction)
|
|
109
|
+
@current_match = index
|
|
110
|
+
if index
|
|
111
|
+
{ cursor: index, status: "#{status_prefix} #{@query}", found: true }
|
|
112
|
+
else
|
|
113
|
+
{ cursor: @origin_cursor, status: "No match: #{@query}", found: false }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def find_match(buffer, query, cursor, direction)
|
|
118
|
+
haystack, needle = self.class.normalized_pair(buffer.to_s, query.to_s)
|
|
119
|
+
cursor = cursor.to_i
|
|
120
|
+
if direction == :backward
|
|
121
|
+
search_from = cursor.positive? ? cursor - 1 : haystack.length
|
|
122
|
+
haystack.rindex(needle, search_from) || haystack.rindex(needle)
|
|
123
|
+
else
|
|
124
|
+
haystack.index(needle, cursor + 1) || haystack.index(needle)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
71
128
|
def status_prefix
|
|
72
129
|
@direction == :backward ? "Search backward:" : "Search:"
|
|
73
130
|
end
|
|
@@ -19,6 +19,7 @@ module Kward
|
|
|
19
19
|
attr_reader :path, :display_path, :language, :original_content, :original_digest, :original_mtime, :original_size
|
|
20
20
|
attr_reader :buffer, :undo_stack, :redo_stack, :kill_buffer, :kill_ring, :last_yank_range, :last_yank_index
|
|
21
21
|
attr_accessor :viewport_row, :viewport_column, :status, :overwrite_confirmed, :quit_confirmed, :search_active, :search_query, :search_direction, :new_file, :editor_mode, :emacs_pending, :readonly, :diff_view
|
|
22
|
+
attr_reader :search_match_ranges
|
|
22
23
|
|
|
23
24
|
def initialize(path:, content:, new_file: false, editor_mode: "modern", readonly: false, diff_view: false, virtual: false, display_path: nil, language: nil)
|
|
24
25
|
@path = virtual ? nil : path.to_s
|
|
@@ -45,6 +46,7 @@ module Kward
|
|
|
45
46
|
@search_active = @search.active?
|
|
46
47
|
@search_query = @search.query
|
|
47
48
|
@search_direction = @search.direction
|
|
49
|
+
@search_match_ranges = []
|
|
48
50
|
@kill_state = EditorKillRing.new
|
|
49
51
|
@kill_buffer = @kill_state.kill_buffer
|
|
50
52
|
@selections = EditorSelections.new(cursor: @cursor, buffer_length: @buffer.length)
|
|
@@ -80,6 +82,7 @@ module Kward
|
|
|
80
82
|
@search_active = other.search_active
|
|
81
83
|
@search_query = other.search_query.dup
|
|
82
84
|
@search_direction = other.search_direction
|
|
85
|
+
@search_match_ranges = other.search_match_ranges.map(&:dup)
|
|
83
86
|
@kill_state = EditorKillRing.new(
|
|
84
87
|
kill_buffer: other.kill_buffer.dup,
|
|
85
88
|
kill_ring: other.kill_ring.map(&:dup),
|
|
@@ -231,6 +234,51 @@ module Kward
|
|
|
231
234
|
sync_vibe_state
|
|
232
235
|
end
|
|
233
236
|
|
|
237
|
+
def vibe_register_types
|
|
238
|
+
@vibe_state.register_types
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def vibe_register_types=(value)
|
|
242
|
+
@vibe_state.register_types = value
|
|
243
|
+
sync_vibe_state
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def vibe_kill_linewise
|
|
247
|
+
@vibe_state.kill_linewise
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def vibe_kill_linewise=(value)
|
|
251
|
+
@vibe_state.kill_linewise = value
|
|
252
|
+
sync_vibe_state
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def vibe_previous_change_cursor
|
|
256
|
+
@vibe_state.previous_change_cursor
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def vibe_previous_change_cursor=(value)
|
|
260
|
+
@vibe_state.previous_change_cursor = value
|
|
261
|
+
sync_vibe_state
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
def vibe_jump_back_list
|
|
265
|
+
@vibe_state.jump_back_list
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def vibe_jump_back_list=(value)
|
|
269
|
+
@vibe_state.jump_back_list = value
|
|
270
|
+
sync_vibe_state
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def vibe_jump_forward_list
|
|
274
|
+
@vibe_state.jump_forward_list
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def vibe_jump_forward_list=(value)
|
|
278
|
+
@vibe_state.jump_forward_list = value
|
|
279
|
+
sync_vibe_state
|
|
280
|
+
end
|
|
281
|
+
|
|
234
282
|
def vibe_macros
|
|
235
283
|
@vibe_state.macros
|
|
236
284
|
end
|
|
@@ -325,7 +373,11 @@ module Kward
|
|
|
325
373
|
end
|
|
326
374
|
|
|
327
375
|
def diff_view?
|
|
328
|
-
@diff_view
|
|
376
|
+
!@diff_view.nil? && @diff_view != false
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def side_by_side_diff_view?
|
|
380
|
+
@diff_view == DiffViewMode::SIDE_BY_SIDE
|
|
329
381
|
end
|
|
330
382
|
|
|
331
383
|
def modern?
|
|
@@ -372,7 +424,7 @@ module Kward
|
|
|
372
424
|
end
|
|
373
425
|
|
|
374
426
|
restore_editor_snapshot(snapshot)
|
|
375
|
-
changed!
|
|
427
|
+
changed!
|
|
376
428
|
@status = "Undo"
|
|
377
429
|
true
|
|
378
430
|
end
|
|
@@ -385,7 +437,7 @@ module Kward
|
|
|
385
437
|
end
|
|
386
438
|
|
|
387
439
|
restore_editor_snapshot(snapshot)
|
|
388
|
-
changed!
|
|
440
|
+
changed!
|
|
389
441
|
@status = "Redo"
|
|
390
442
|
true
|
|
391
443
|
end
|
|
@@ -909,27 +961,28 @@ module Kward
|
|
|
909
961
|
end
|
|
910
962
|
|
|
911
963
|
def begin_search(direction = :forward)
|
|
912
|
-
@status = @search.begin(direction)
|
|
964
|
+
@status = @search.begin(direction, cursor: @cursor)
|
|
913
965
|
sync_search_state
|
|
914
966
|
true
|
|
915
967
|
end
|
|
916
968
|
|
|
917
|
-
def cancel_search
|
|
918
|
-
@
|
|
919
|
-
|
|
969
|
+
def cancel_search(restore_cursor: true)
|
|
970
|
+
apply_search_result(@search.cancel(restore_cursor: restore_cursor))
|
|
971
|
+
@search_match_ranges = []
|
|
920
972
|
true
|
|
921
973
|
end
|
|
922
974
|
|
|
923
|
-
def
|
|
924
|
-
@
|
|
925
|
-
sync_search_state
|
|
975
|
+
def clear_search_highlights
|
|
976
|
+
@search_match_ranges = []
|
|
926
977
|
true
|
|
927
978
|
end
|
|
928
979
|
|
|
980
|
+
def append_search(text)
|
|
981
|
+
apply_search_result(@search.append(text, buffer: @buffer, cursor: @cursor))
|
|
982
|
+
end
|
|
983
|
+
|
|
929
984
|
def delete_search_character
|
|
930
|
-
@
|
|
931
|
-
sync_search_state
|
|
932
|
-
true
|
|
985
|
+
apply_search_result(@search.delete_character(buffer: @buffer, cursor: @cursor))
|
|
933
986
|
end
|
|
934
987
|
|
|
935
988
|
def confirm_search
|
|
@@ -1224,6 +1277,7 @@ module Kward
|
|
|
1224
1277
|
@search_active = @search.active?
|
|
1225
1278
|
@search_query = @search.query
|
|
1226
1279
|
@search_direction = @search.direction
|
|
1280
|
+
@search_match_ranges = EditorSearch.match_ranges(@buffer, @search_query)
|
|
1227
1281
|
end
|
|
1228
1282
|
|
|
1229
1283
|
def sync_vibe_state
|
|
@@ -1236,6 +1290,11 @@ module Kward
|
|
|
1236
1290
|
@vibe_visual_block_insert = @vibe_state.visual_block_insert
|
|
1237
1291
|
@vibe_marks = @vibe_state.marks
|
|
1238
1292
|
@vibe_registers = @vibe_state.registers
|
|
1293
|
+
@vibe_register_types = @vibe_state.register_types
|
|
1294
|
+
@vibe_kill_linewise = @vibe_state.kill_linewise
|
|
1295
|
+
@vibe_previous_change_cursor = @vibe_state.previous_change_cursor
|
|
1296
|
+
@vibe_jump_back_list = @vibe_state.jump_back_list
|
|
1297
|
+
@vibe_jump_forward_list = @vibe_state.jump_forward_list
|
|
1239
1298
|
@vibe_macros = @vibe_state.macros
|
|
1240
1299
|
@vibe_recording_macro = @vibe_state.recording_macro
|
|
1241
1300
|
@vibe_last_macro = @vibe_state.last_macro
|
|
@@ -6,7 +6,8 @@ module Kward
|
|
|
6
6
|
class VibeEditorState
|
|
7
7
|
attr_accessor :mode, :pending, :command, :last_change, :last_find
|
|
8
8
|
attr_accessor :last_visual_selection, :visual_block_insert
|
|
9
|
-
attr_accessor :marks, :registers, :macros, :recording_macro, :last_macro
|
|
9
|
+
attr_accessor :marks, :registers, :register_types, :macros, :recording_macro, :last_macro
|
|
10
|
+
attr_accessor :kill_linewise, :previous_change_cursor, :jump_back_list, :jump_forward_list
|
|
10
11
|
|
|
11
12
|
def initialize(editor_mode: "modern")
|
|
12
13
|
@mode = editor_mode == "vibe" ? "normal" : nil
|
|
@@ -18,6 +19,11 @@ module Kward
|
|
|
18
19
|
@visual_block_insert = nil
|
|
19
20
|
@marks = {}
|
|
20
21
|
@registers = {}
|
|
22
|
+
@register_types = {}
|
|
23
|
+
@kill_linewise = false
|
|
24
|
+
@previous_change_cursor = nil
|
|
25
|
+
@jump_back_list = []
|
|
26
|
+
@jump_forward_list = []
|
|
21
27
|
@macros = {}
|
|
22
28
|
@recording_macro = nil
|
|
23
29
|
@last_macro = nil
|
|
@@ -34,6 +40,11 @@ module Kward
|
|
|
34
40
|
state.visual_block_insert = other.visual_block_insert&.dup
|
|
35
41
|
state.marks = other.marks.transform_values(&:dup)
|
|
36
42
|
state.registers = other.registers.transform_values(&:dup)
|
|
43
|
+
state.register_types = other.register_types.transform_values(&:dup)
|
|
44
|
+
state.kill_linewise = other.kill_linewise
|
|
45
|
+
state.previous_change_cursor = other.previous_change_cursor
|
|
46
|
+
state.jump_back_list = other.jump_back_list.map(&:dup)
|
|
47
|
+
state.jump_forward_list = other.jump_forward_list.map(&:dup)
|
|
37
48
|
state.macros = other.macros.transform_values(&:dup)
|
|
38
49
|
state.recording_macro = other.recording_macro
|
|
39
50
|
state.last_macro = other.last_macro
|
|
@@ -73,6 +73,8 @@ module Kward
|
|
|
73
73
|
queue_pending_keys(sequence[:remaining]) if sequence[:remaining] && !sequence[:remaining].empty?
|
|
74
74
|
|
|
75
75
|
case code
|
|
76
|
+
when 9
|
|
77
|
+
modified_tab_sequence?(sequence) ? true : select_toggle_search
|
|
76
78
|
when 13
|
|
77
79
|
select_input_active? ? select_input_action_result : select_current_choice
|
|
78
80
|
when 27
|
|
@@ -501,25 +503,43 @@ module Kward
|
|
|
501
503
|
|
|
502
504
|
def select_typed_key(key)
|
|
503
505
|
return select_insert_key(key) if select_input_active?
|
|
504
|
-
return
|
|
506
|
+
return select_toggle_search if select_search_toggle_key?(key)
|
|
505
507
|
return select_action_key(key) unless select_search_active?
|
|
506
508
|
|
|
507
509
|
select_insert_key(key)
|
|
508
510
|
end
|
|
509
511
|
|
|
512
|
+
def select_search_toggle_key?(key)
|
|
513
|
+
TerminalKeys::TAB.include?(key)
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def modified_tab_sequence?(sequence)
|
|
517
|
+
modifier = sequence[:modifier]
|
|
518
|
+
modifier && modifier != 1
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def select_toggle_search
|
|
522
|
+
select_search_active? ? select_pause_search : select_begin_search
|
|
523
|
+
end
|
|
524
|
+
|
|
510
525
|
def select_begin_search
|
|
511
526
|
return unless @select_state
|
|
512
527
|
|
|
513
528
|
@select_state[:search_active] = true
|
|
514
|
-
self.composer_input = ""
|
|
515
|
-
self.composer_cursor = 0
|
|
516
529
|
true
|
|
517
530
|
end
|
|
518
531
|
|
|
519
|
-
def
|
|
532
|
+
def select_pause_search
|
|
520
533
|
return unless @select_state
|
|
521
534
|
|
|
522
535
|
@select_state[:search_active] = false
|
|
536
|
+
true
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def select_cancel_search
|
|
540
|
+
return unless @select_state
|
|
541
|
+
|
|
542
|
+
select_pause_search
|
|
523
543
|
self.composer_input = ""
|
|
524
544
|
self.composer_cursor = 0
|
|
525
545
|
@select_state[:selection_index] = 0
|
|
@@ -607,7 +627,7 @@ module Kward
|
|
|
607
627
|
|
|
608
628
|
def selection_matches
|
|
609
629
|
choices = @select_state ? @select_state[:choices] : []
|
|
610
|
-
filter = select_search_active? ? composer_input.downcase.strip : ""
|
|
630
|
+
filter = (select_search_active? || !composer_input.strip.empty?) ? composer_input.downcase.strip : ""
|
|
611
631
|
matches = filter.empty? ? choices : choices.select { |choice| choice.downcase.include?(filter) }
|
|
612
632
|
clamp_selection_index(matches.length)
|
|
613
633
|
matches
|
|
@@ -666,7 +686,7 @@ module Kward
|
|
|
666
686
|
return "Renaming · Enter save · Esc cancel" if select_input_active?
|
|
667
687
|
|
|
668
688
|
text = "↑/↓ select · Enter open"
|
|
669
|
-
text = "#{text} ·
|
|
689
|
+
text = "#{text} · Tab search" unless select_search_active?
|
|
670
690
|
action_keys = @select_state ? @select_state[:action_keys].to_h : {}
|
|
671
691
|
action_keys.each do |key, action|
|
|
672
692
|
text = "#{text} · #{key} #{action[:action]}"
|
|
@@ -12,6 +12,7 @@ require_relative "ansi"
|
|
|
12
12
|
require_relative "terminal_sequences"
|
|
13
13
|
require_relative "terminal_keys"
|
|
14
14
|
require_relative "editor_mode"
|
|
15
|
+
require_relative "diff_view_mode"
|
|
15
16
|
require_relative "prompt_interface/banner"
|
|
16
17
|
require_relative "prompt_interface/composer_state"
|
|
17
18
|
require_relative "prompt_interface/editor/state"
|
|
@@ -125,7 +126,7 @@ module Kward
|
|
|
125
126
|
end
|
|
126
127
|
end
|
|
127
128
|
|
|
128
|
-
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil)
|
|
129
|
+
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil, diff_view: "auto", diff_view_source: nil)
|
|
129
130
|
@input_io = input
|
|
130
131
|
@output_io = output
|
|
131
132
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
@@ -206,6 +207,8 @@ module Kward
|
|
|
206
207
|
@editor_bar_cursor_source = editor_bar_cursor_source
|
|
207
208
|
@editor_line_numbers = normalize_editor_line_numbers(editor_line_numbers)
|
|
208
209
|
@editor_line_numbers_source = editor_line_numbers_source
|
|
210
|
+
@diff_view = DiffViewMode.normalize(diff_view)
|
|
211
|
+
@diff_view_source = diff_view_source
|
|
209
212
|
end
|
|
210
213
|
|
|
211
214
|
def start(render: true)
|
|
@@ -8,9 +8,8 @@ module Kward
|
|
|
8
8
|
{ name: "exit", description: "Exit the interactive session.", argument_hint: "" },
|
|
9
9
|
{ name: "quit", description: "Exit the interactive session.", argument_hint: "" },
|
|
10
10
|
{ name: "new", description: "Start a new session.", argument_hint: "" },
|
|
11
|
-
{ name: "
|
|
12
|
-
{ name: "resume", description: "Alias for /
|
|
13
|
-
{ name: "name", description: "Name or clear the current session.", argument_hint: "[name]" },
|
|
11
|
+
{ name: "session", description: "Open saved sessions or name the current session.", argument_hint: "[path]|name [name]" },
|
|
12
|
+
{ name: "resume", description: "Alias for /session.", argument_hint: "[path]" },
|
|
14
13
|
{ name: "rename", description: "Rename the current session.", argument_hint: "<name>" },
|
|
15
14
|
{ name: "clone", description: "Clone the current session.", argument_hint: "" },
|
|
16
15
|
{ name: "fork", description: "Fork from an earlier prompt into a new session.", argument_hint: "" },
|
|
@@ -36,6 +35,7 @@ module Kward
|
|
|
36
35
|
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name]" },
|
|
37
36
|
{ name: "status", description: "Show the current status message.", argument_hint: "" },
|
|
38
37
|
{ name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
|
|
38
|
+
{ name: "hooks", description: "Inspect lifecycle hooks.", argument_hint: "[list|events|logs|doctor|trust|untrust]" },
|
|
39
39
|
{ name: "memory", description: "Inspect and manage Kward memory.", argument_hint: "[enable|disable|auto-summary|core|add|list|forget|promote|relax|inspect|why|summarize]" }
|
|
40
40
|
].freeze
|
|
41
41
|
BUILTIN_RESERVED_COMMAND_NAMES = BUILTIN_COMMANDS.map { |command| command[:name] }.freeze
|
data/lib/kward/prompts.rb
CHANGED
|
@@ -25,7 +25,8 @@ module Kward
|
|
|
25
25
|
sections << prompt_section("Persona", persona_prompt(workspace_root, model: model, reasoning_effort: reasoning_effort, now: now))
|
|
26
26
|
sections << prompt_section("Plugin context", plugin_context) unless plugin_context.to_s.empty?
|
|
27
27
|
end
|
|
28
|
-
|
|
28
|
+
skills = ConfigFiles.skills(workspace_root: workspace_root)
|
|
29
|
+
sections << prompt_section("Configured skills", skills_prompt(skills), source: skills.empty? ? nil : File.join(ConfigFiles.config_dir, "skills"))
|
|
29
30
|
sections << prompt_section(workspace_agents_context_label(workspace_root), workspace_agents_context(workspace_root), source: ConfigFiles.workspace_agents_file?(workspace_root) ? ConfigFiles.workspace_agents_path(workspace_root) : nil)
|
|
30
31
|
sections.compact
|
|
31
32
|
end
|
|
@@ -97,18 +98,17 @@ module Kward
|
|
|
97
98
|
{ label: label, content: content, source: source }
|
|
98
99
|
end
|
|
99
100
|
|
|
100
|
-
def skills_prompt
|
|
101
|
-
skills = ConfigFiles.skills
|
|
101
|
+
def skills_prompt(skills = ConfigFiles.skills)
|
|
102
102
|
return nil if skills.empty?
|
|
103
103
|
|
|
104
104
|
lines = [
|
|
105
|
-
"
|
|
106
|
-
"When a task matches a skill, use read_skill to load its instructions before proceeding.",
|
|
105
|
+
"Agent Skills are available.",
|
|
106
|
+
"When a task matches a skill's description, use read_skill to load its instructions before proceeding.",
|
|
107
|
+
"Use read_skill with a relative path to read referenced files inside a skill directory.",
|
|
107
108
|
"Available skills:"
|
|
108
109
|
]
|
|
109
110
|
skills.each do |skill|
|
|
110
|
-
|
|
111
|
-
lines << "- #{skill.name}: #{description}"
|
|
111
|
+
lines << "- #{skill.name}: #{skill.description}"
|
|
112
112
|
end
|
|
113
113
|
lines.join("\n")
|
|
114
114
|
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
require_relative "../mcp/server_config"
|
|
2
|
+
require_relative "redactor"
|
|
3
|
+
|
|
4
|
+
# Namespace for the Kward CLI agent runtime.
|
|
5
|
+
module Kward
|
|
6
|
+
# JSON-RPC backend namespace used by UI clients.
|
|
7
|
+
module RPC
|
|
8
|
+
# Builds RPC-visible MCP server status payloads.
|
|
9
|
+
class MCPStatus
|
|
10
|
+
def initialize(config_manager:)
|
|
11
|
+
@config_manager = config_manager
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
config = @config_manager.read(redacted: false)
|
|
16
|
+
clients = MCP::ServerConfig.clients_from_config(config).to_h { |client| [client.name, client] }
|
|
17
|
+
servers = MCP::ServerConfig.configured_servers(config).map do |name, settings|
|
|
18
|
+
server_status(name, settings, clients[name.to_s])
|
|
19
|
+
end
|
|
20
|
+
{ servers: servers }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def server_status(name, settings, client)
|
|
26
|
+
base = { name: name.to_s, transport: "stdio", toolCount: 0 }
|
|
27
|
+
unless client
|
|
28
|
+
return base.merge(status: "unavailable", error: "command not configured") if settings["command"].to_s.empty?
|
|
29
|
+
|
|
30
|
+
return base.merge(status: "unavailable", error: "unsupported MCP server configuration")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
tools = client.list_tools
|
|
34
|
+
base.merge(status: "available", toolCount: tools.length)
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
base.merge(status: "unavailable", toolCount: 0, error: redacted_error(e.message, client))
|
|
37
|
+
ensure
|
|
38
|
+
client&.close
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def redacted_error(message, client)
|
|
42
|
+
text = Redactor.redact_string(message.to_s)
|
|
43
|
+
command = client&.transport&.respond_to?(:command) ? client.transport.command.to_s : ""
|
|
44
|
+
return text if command.empty?
|
|
45
|
+
|
|
46
|
+
text.gsub(command, File.basename(command))
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -17,6 +17,32 @@ module Kward
|
|
|
17
17
|
@pending_requests = {}
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def request_tool_approval(tool_call_id:, tool_name:, args:, cancellation: nil)
|
|
21
|
+
request_id = SecureRandom.uuid
|
|
22
|
+
@mutex.synchronize { @pending_requests[request_id] = true }
|
|
23
|
+
cancellation&.on_cancel { cancel_request(request_id) }
|
|
24
|
+
unless cancellation&.cancelled?
|
|
25
|
+
@server.notify("tool/approvalRequested", {
|
|
26
|
+
sessionId: @session_id,
|
|
27
|
+
approvalRequestId: request_id,
|
|
28
|
+
toolCallId: tool_call_id,
|
|
29
|
+
toolName: tool_name,
|
|
30
|
+
args: args
|
|
31
|
+
})
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@mutex.synchronize do
|
|
35
|
+
@condition.wait(@mutex) until @answers.key?(request_id)
|
|
36
|
+
answer = @answers.delete(request_id)
|
|
37
|
+
@pending_requests.delete(request_id)
|
|
38
|
+
approved_answer?(answer)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def answer_tool_approval(request_id, approved:)
|
|
43
|
+
answer_raw(request_id, !!approved)
|
|
44
|
+
end
|
|
45
|
+
|
|
20
46
|
def ask_user_question(questions, cancellation: nil)
|
|
21
47
|
questions = validate_questions(questions)
|
|
22
48
|
request_id = SecureRandom.uuid
|
|
@@ -41,28 +67,36 @@ module Kward
|
|
|
41
67
|
end
|
|
42
68
|
|
|
43
69
|
def answer(request_id, answers)
|
|
70
|
+
answer_raw(request_id, normalize_answers(answers))
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def cancel_request(request_id)
|
|
44
74
|
@mutex.synchronize do
|
|
45
75
|
request_id = request_id.to_s
|
|
46
76
|
return unless @pending_requests.key?(request_id)
|
|
47
77
|
return if @answers.key?(request_id)
|
|
48
78
|
|
|
49
|
-
@answers[request_id] =
|
|
79
|
+
@answers[request_id] = nil
|
|
50
80
|
@condition.broadcast
|
|
51
81
|
end
|
|
52
82
|
end
|
|
53
83
|
|
|
54
|
-
|
|
84
|
+
private
|
|
85
|
+
|
|
86
|
+
def answer_raw(request_id, value)
|
|
55
87
|
@mutex.synchronize do
|
|
56
88
|
request_id = request_id.to_s
|
|
57
89
|
return unless @pending_requests.key?(request_id)
|
|
58
90
|
return if @answers.key?(request_id)
|
|
59
91
|
|
|
60
|
-
@answers[request_id] =
|
|
92
|
+
@answers[request_id] = value
|
|
61
93
|
@condition.broadcast
|
|
62
94
|
end
|
|
63
95
|
end
|
|
64
96
|
|
|
65
|
-
|
|
97
|
+
def approved_answer?(answer)
|
|
98
|
+
answer == true
|
|
99
|
+
end
|
|
66
100
|
|
|
67
101
|
def normalize_answers(answers)
|
|
68
102
|
return nil if answers.nil?
|