kward 0.71.0 → 0.72.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 +41 -1
- 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 +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -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 +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -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/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/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -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 +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -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 +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -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 +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -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 +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -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 +299 -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 +387 -35
- 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 +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- 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 +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- 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/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -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 +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -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 +7 -0
- data/lib/kward/workspace.rb +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +53 -1
|
@@ -1,21 +1,44 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "find"
|
|
1
3
|
require "io/console"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "rbconfig"
|
|
2
6
|
require "thread"
|
|
7
|
+
require_relative "project_files"
|
|
8
|
+
require_relative "prompt_history"
|
|
3
9
|
require "tty-cursor"
|
|
4
10
|
require "tty-reader"
|
|
5
11
|
require "tty-screen"
|
|
6
12
|
require_relative "ansi"
|
|
13
|
+
require_relative "editor_mode"
|
|
7
14
|
require_relative "prompt_interface/banner"
|
|
8
15
|
require_relative "prompt_interface/composer_state"
|
|
16
|
+
require_relative "prompt_interface/editor/state"
|
|
9
17
|
require_relative "prompt_interface/transcript_buffer"
|
|
10
18
|
require_relative "prompt_interface/transcript_renderer"
|
|
11
19
|
require_relative "prompt_interface/prompt_renderer"
|
|
12
20
|
require_relative "prompt_interface/stream_state"
|
|
13
21
|
require_relative "prompt_interface/slash_overlay"
|
|
22
|
+
require_relative "prompt_interface/file_overlay"
|
|
23
|
+
require_relative "prompt_interface/project_browser"
|
|
14
24
|
require_relative "prompt_interface/selection_prompt"
|
|
15
25
|
require_relative "prompt_interface/question_prompt"
|
|
26
|
+
require_relative "prompt_interface/git_prompt"
|
|
16
27
|
require_relative "prompt_interface/overlay_renderer"
|
|
28
|
+
require_relative "prompt_interface/editor/renderer"
|
|
29
|
+
require_relative "prompt_interface/editor/syntax_highlighter"
|
|
30
|
+
require_relative "prompt_interface/editor/auto_close_pairs"
|
|
31
|
+
require_relative "prompt_interface/editor/endwise"
|
|
32
|
+
require_relative "prompt_interface/editor/auto_indent"
|
|
17
33
|
require_relative "prompt_interface/composer_renderer"
|
|
18
34
|
require_relative "prompt_interface/composer_controller"
|
|
35
|
+
require_relative "prompt_interface/editor/modes/modern"
|
|
36
|
+
require_relative "prompt_interface/editor/modes/emacs"
|
|
37
|
+
require_relative "prompt_interface/editor/modes/vibe"
|
|
38
|
+
require_relative "prompt_interface/editor/controller"
|
|
39
|
+
require_relative "prompt_interface/interactive/controller"
|
|
40
|
+
require_relative "prompt_interface/interactive/renderer"
|
|
41
|
+
require_relative "prompt_interface/interactive/state"
|
|
19
42
|
require_relative "prompt_interface/layout"
|
|
20
43
|
require_relative "prompt_interface/screen"
|
|
21
44
|
require_relative "prompt_interface/key_handler"
|
|
@@ -39,23 +62,38 @@ module Kward
|
|
|
39
62
|
SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
40
63
|
SPINNER_INTERVAL = 0.1
|
|
41
64
|
FOOTER_REFRESH_INTERVAL = 1.0
|
|
65
|
+
COMPOSER_STATUS_REFRESH_INTERVAL = 1.0
|
|
42
66
|
COMPOSER_MAX_INPUT_ROWS = 6
|
|
43
67
|
TRANSCRIPT_BUFFER_LIMIT = 200_000
|
|
44
68
|
BANNER_MESSAGE = Banner::MESSAGE
|
|
45
69
|
|
|
46
70
|
include SlashOverlay
|
|
71
|
+
include FileOverlay
|
|
72
|
+
include ProjectBrowser
|
|
47
73
|
include SelectionPrompt
|
|
48
74
|
include QuestionPrompt
|
|
75
|
+
include GitPrompt
|
|
49
76
|
include OverlayRenderer
|
|
77
|
+
include EditorRenderer
|
|
78
|
+
include EditorSyntaxHighlighter
|
|
79
|
+
include EditorAutoClosePairs
|
|
80
|
+
include EditorEndwise
|
|
81
|
+
include EditorAutoIndent
|
|
50
82
|
include ComposerRenderer
|
|
51
83
|
include ComposerController
|
|
84
|
+
include ModernEditorMode
|
|
85
|
+
include EmacsEditorMode
|
|
86
|
+
include VibeEditorMode
|
|
87
|
+
include EditorController
|
|
88
|
+
include InteractiveRenderer
|
|
89
|
+
include InteractiveState
|
|
52
90
|
include Layout
|
|
53
91
|
include Screen
|
|
54
92
|
include KeyHandler
|
|
55
93
|
include RuntimeState
|
|
56
94
|
include TranscriptRenderer
|
|
57
95
|
include PromptRenderer
|
|
58
|
-
KEYBOARD_PROTOCOL_ENABLE = "\e[>
|
|
96
|
+
KEYBOARD_PROTOCOL_ENABLE = "\e[>25u".freeze
|
|
59
97
|
KEYBOARD_PROTOCOL_RESTORE = "\e[<u".freeze
|
|
60
98
|
BRACKETED_PASTE_ENABLE = "\e[?2004h".freeze
|
|
61
99
|
BRACKETED_PASTE_RESTORE = "\e[?2004l".freeze
|
|
@@ -65,6 +103,8 @@ module Kward
|
|
|
65
103
|
SYNCHRONIZED_OUTPUT_DISABLE = "\e[?2026l".freeze
|
|
66
104
|
CURSOR_SHOW = "\e[?25h".freeze
|
|
67
105
|
CURSOR_HIDE = "\e[?25l".freeze
|
|
106
|
+
CURSOR_SHAPE_DEFAULT = "\e[0 q".freeze
|
|
107
|
+
CURSOR_SHAPE_BAR = "\e[6 q".freeze
|
|
68
108
|
SHIFT_ENTER_SEQUENCES = ["\e[13;2u", "\e[13;2~", "\e[27;2;13~", "\e\r", "\e\n"].freeze
|
|
69
109
|
EXIT_INPUT = :exit_input
|
|
70
110
|
CANCEL_INPUT = :cancel_input
|
|
@@ -82,12 +122,14 @@ module Kward
|
|
|
82
122
|
end
|
|
83
123
|
end
|
|
84
124
|
|
|
85
|
-
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)
|
|
125
|
+
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)
|
|
86
126
|
@input_io = input
|
|
87
127
|
@output_io = output
|
|
88
128
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
89
129
|
@mutex = Mutex.new
|
|
130
|
+
@prompt_history = prompt_history
|
|
90
131
|
@composer = ComposerState.new
|
|
132
|
+
load_history(@prompt_history.values) if @prompt_history
|
|
91
133
|
self.composer_input = @composer.input
|
|
92
134
|
self.composer_cursor = @composer.cursor
|
|
93
135
|
@started = false
|
|
@@ -99,6 +141,8 @@ module Kward
|
|
|
99
141
|
@spinner_frame_index = 0
|
|
100
142
|
@last_spinner_tick = monotonic_now
|
|
101
143
|
@last_footer_refresh = monotonic_now
|
|
144
|
+
@last_composer_status_refresh = 0.0
|
|
145
|
+
@cached_composer_status_text = nil
|
|
102
146
|
@prompt_label = "You>"
|
|
103
147
|
@assistant_label = "Assistant"
|
|
104
148
|
@stream_state = StreamState.new
|
|
@@ -109,18 +153,32 @@ module Kward
|
|
|
109
153
|
@transcript_viewport_rows = 0
|
|
110
154
|
@restoring_transcript = false
|
|
111
155
|
@pending_keys = []
|
|
156
|
+
@completion_provider = nil
|
|
112
157
|
@original_console_mode = nil
|
|
113
158
|
@raw_mode_active = false
|
|
114
159
|
@slash_commands = normalize_slash_commands(slash_commands)
|
|
115
160
|
@slash_selection_index = 0
|
|
116
161
|
@slash_overlay_dismissed_input = nil
|
|
162
|
+
@file_selection_index = 0
|
|
163
|
+
@file_overlay_dismissed_token = nil
|
|
164
|
+
@file_open_dismissed_token = nil
|
|
165
|
+
@file_editor_open_status = nil
|
|
166
|
+
@file_mention_paths = nil
|
|
167
|
+
@project_browser_state = nil
|
|
168
|
+
@project_browser_restore_after_editor = false
|
|
169
|
+
@editor_state = nil
|
|
170
|
+
@interactive_state = nil
|
|
171
|
+
@last_interactive_tick = monotonic_now
|
|
117
172
|
@select_state = nil
|
|
118
173
|
@question_state = nil
|
|
174
|
+
@question_prompt_active = false
|
|
175
|
+
@git_state = nil
|
|
119
176
|
@last_width = screen_width
|
|
120
177
|
@last_height = screen_height
|
|
121
178
|
@reserved_rows = 0
|
|
122
179
|
@color_enabled = ANSI.enabled?(output)
|
|
123
180
|
@cursor_visible = true
|
|
181
|
+
@editor_bar_cursor_active = false
|
|
124
182
|
@synchronized_output_depth = 0
|
|
125
183
|
@overlay_settings = normalize_overlay_settings(overlay_settings)
|
|
126
184
|
@footer = footer
|
|
@@ -129,6 +187,21 @@ module Kward
|
|
|
129
187
|
@attachment_badges = attachment_badges
|
|
130
188
|
@attachment_parser = attachment_parser
|
|
131
189
|
@banner = Banner.new(message: banner_message, screen_height: method(:screen_height))
|
|
190
|
+
@tabs = []
|
|
191
|
+
@active_tab_index = 0
|
|
192
|
+
@tab_keybindings = normalize_tab_keybindings(tab_keybindings)
|
|
193
|
+
@editor_mode = normalize_editor_mode(editor_mode)
|
|
194
|
+
@editor_mode_source = editor_mode_source
|
|
195
|
+
@editor_auto_indent = editor_auto_indent != false
|
|
196
|
+
@editor_auto_indent_source = editor_auto_indent_source
|
|
197
|
+
@editor_auto_close_pairs = editor_auto_close_pairs != false
|
|
198
|
+
@editor_auto_close_pairs_source = editor_auto_close_pairs_source
|
|
199
|
+
@editor_soft_wrap = editor_soft_wrap != false
|
|
200
|
+
@editor_soft_wrap_source = editor_soft_wrap_source
|
|
201
|
+
@editor_bar_cursor = editor_bar_cursor != false
|
|
202
|
+
@editor_bar_cursor_source = editor_bar_cursor_source
|
|
203
|
+
@editor_line_numbers = normalize_editor_line_numbers(editor_line_numbers)
|
|
204
|
+
@editor_line_numbers_source = editor_line_numbers_source
|
|
132
205
|
end
|
|
133
206
|
|
|
134
207
|
def start(render: true)
|
|
@@ -138,6 +211,7 @@ module Kward
|
|
|
138
211
|
enter_raw_mode_locked
|
|
139
212
|
@started = true
|
|
140
213
|
@asking = true
|
|
214
|
+
disable_editor_mouse_reporting(force: true) unless editor_active?
|
|
141
215
|
@output_io.print(KEYBOARD_PROTOCOL_ENABLE)
|
|
142
216
|
@output_io.print(BRACKETED_PASTE_ENABLE)
|
|
143
217
|
render_prompt_locked if render
|
|
@@ -150,8 +224,10 @@ module Kward
|
|
|
150
224
|
|
|
151
225
|
clear_prompt_for_output_locked
|
|
152
226
|
restore_scroll_region_locked
|
|
227
|
+
disable_editor_mouse_reporting(force: true)
|
|
153
228
|
@output_io.print(BRACKETED_PASTE_RESTORE)
|
|
154
229
|
@output_io.print(KEYBOARD_PROTOCOL_RESTORE)
|
|
230
|
+
restore_editor_cursor_shape_locked
|
|
155
231
|
set_cursor_visible_locked(true, force: true)
|
|
156
232
|
@output_io.puts
|
|
157
233
|
@output_io.flush
|
|
@@ -220,6 +296,54 @@ module Kward
|
|
|
220
296
|
end
|
|
221
297
|
end
|
|
222
298
|
|
|
299
|
+
def with_completion_provider(provider)
|
|
300
|
+
previous = @completion_provider
|
|
301
|
+
@completion_provider = provider
|
|
302
|
+
yield
|
|
303
|
+
ensure
|
|
304
|
+
@completion_provider = previous
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def editing_file?
|
|
308
|
+
@mutex.synchronize { editor_active? }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def edit_file(path, base_dir: Dir.pwd, allow_new: true)
|
|
312
|
+
start(render: false)
|
|
313
|
+
opened = @mutex.synchronize do
|
|
314
|
+
open_editor(path, allow_new: allow_new, base_dir: base_dir, restrict_to_workspace: false).tap do
|
|
315
|
+
render_prompt_locked
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
return false unless opened
|
|
319
|
+
|
|
320
|
+
run_editor
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def run_editor
|
|
324
|
+
loop do
|
|
325
|
+
key = read_key(nonblock: true)
|
|
326
|
+
action = nil
|
|
327
|
+
editor_open = @mutex.synchronize do
|
|
328
|
+
if key.nil?
|
|
329
|
+
resized = handle_resize_locked
|
|
330
|
+
footer_refreshed = tick_footer_locked
|
|
331
|
+
render_prompt_locked if resized || footer_refreshed
|
|
332
|
+
else
|
|
333
|
+
result = handle_key(key)
|
|
334
|
+
action = result if prompt_action_result?(result)
|
|
335
|
+
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
|
|
336
|
+
end
|
|
337
|
+
editor_active?
|
|
338
|
+
end
|
|
339
|
+
return action if action
|
|
340
|
+
break unless editor_open
|
|
341
|
+
|
|
342
|
+
sleep 0.02 if key.nil?
|
|
343
|
+
end
|
|
344
|
+
true
|
|
345
|
+
end
|
|
346
|
+
|
|
223
347
|
def ask(message = "You>")
|
|
224
348
|
was_composing = @started && @asking
|
|
225
349
|
start
|
|
@@ -250,10 +374,10 @@ module Kward
|
|
|
250
374
|
render_prompt_locked if resized || footer_refreshed
|
|
251
375
|
else
|
|
252
376
|
result = handle_key(key)
|
|
253
|
-
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT
|
|
377
|
+
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
|
|
254
378
|
end
|
|
255
379
|
end
|
|
256
|
-
return result if result.is_a?(String)
|
|
380
|
+
return result if result.is_a?(String) || prompt_action_result?(result)
|
|
257
381
|
return nil if result == EXIT_INPUT
|
|
258
382
|
|
|
259
383
|
sleep 0.02 if key.nil?
|
|
@@ -323,7 +447,10 @@ module Kward
|
|
|
323
447
|
start
|
|
324
448
|
saved_state = nil
|
|
325
449
|
answers = []
|
|
326
|
-
@mutex.synchronize
|
|
450
|
+
@mutex.synchronize do
|
|
451
|
+
@question_prompt_active = true
|
|
452
|
+
saved_state = begin_question_prompt_state
|
|
453
|
+
end
|
|
327
454
|
|
|
328
455
|
questions.each_with_index do |question, index|
|
|
329
456
|
answer = ask_single_user_question(question, index + 1, questions.length)
|
|
@@ -342,7 +469,127 @@ module Kward
|
|
|
342
469
|
end
|
|
343
470
|
|
|
344
471
|
def modal_active?
|
|
345
|
-
@mutex.synchronize {
|
|
472
|
+
@mutex.synchronize { modal_active_locked? }
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def interactive_active?
|
|
476
|
+
@mutex.synchronize { interactive_active_locked? }
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def interactive_exited?
|
|
480
|
+
@mutex.synchronize do
|
|
481
|
+
return false unless @interactive_state
|
|
482
|
+
|
|
483
|
+
@interactive_state[:controller].exited?
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def finish_interactive
|
|
488
|
+
@mutex.synchronize do
|
|
489
|
+
return unless @interactive_state
|
|
490
|
+
|
|
491
|
+
snapshot = @interactive_state[:snapshot]
|
|
492
|
+
@interactive_state = nil
|
|
493
|
+
restore_composer_snapshot_locked(snapshot)
|
|
494
|
+
redraw_screen_locked if @started
|
|
495
|
+
@output_io.flush
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def start_interactive(title:, rows:, fps:)
|
|
500
|
+
snapshot = composer_snapshot
|
|
501
|
+
controller = InteractiveController.new(width: interactive_canvas_width, height: rows, fps: fps)
|
|
502
|
+
start
|
|
503
|
+
@mutex.synchronize do
|
|
504
|
+
@interactive_state = {
|
|
505
|
+
title: title.to_s,
|
|
506
|
+
rows: rows,
|
|
507
|
+
controller: controller,
|
|
508
|
+
snapshot: snapshot
|
|
509
|
+
}
|
|
510
|
+
@last_interactive_tick = monotonic_now
|
|
511
|
+
@asking = true
|
|
512
|
+
@busy = false
|
|
513
|
+
@last_composer_rows = []
|
|
514
|
+
render_prompt_locked
|
|
515
|
+
end
|
|
516
|
+
controller
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def update_tabs(labels:, active_index: 0)
|
|
520
|
+
@mutex.synchronize do
|
|
521
|
+
@tabs = Array(labels).map { |label| normalize_tab_label(label) }
|
|
522
|
+
@active_tab_index = active_index.to_i
|
|
523
|
+
render_prompt_locked if @started && @asking
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def composer_snapshot
|
|
528
|
+
@mutex.synchronize do
|
|
529
|
+
{
|
|
530
|
+
composer: @composer,
|
|
531
|
+
prompt_label: @prompt_label
|
|
532
|
+
}
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def tab_view_snapshot
|
|
537
|
+
@mutex.synchronize do
|
|
538
|
+
{
|
|
539
|
+
composer: @composer.dup,
|
|
540
|
+
prompt_label: @prompt_label.dup,
|
|
541
|
+
editor_state: @editor_state&.dup,
|
|
542
|
+
transcript_buffer: @transcript_buffer.dup,
|
|
543
|
+
transcript_viewport_rows: @transcript_viewport_rows,
|
|
544
|
+
stream_state: @stream_state.dup
|
|
545
|
+
}
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def restore_composer_snapshot(snapshot)
|
|
550
|
+
@mutex.synchronize do
|
|
551
|
+
restore_composer_snapshot_locked(snapshot)
|
|
552
|
+
restore_editor_snapshot_locked(snapshot)
|
|
553
|
+
redraw_screen_locked if @started
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def restore_tab_view_snapshot(snapshot)
|
|
558
|
+
@mutex.synchronize do
|
|
559
|
+
restore_composer_snapshot_locked(snapshot)
|
|
560
|
+
restore_editor_snapshot_locked(snapshot)
|
|
561
|
+
@transcript_buffer = snapshot[:transcript_buffer] || TranscriptBuffer.new(limit: TRANSCRIPT_BUFFER_LIMIT)
|
|
562
|
+
@transcript_viewport_rows = snapshot[:transcript_viewport_rows].to_i
|
|
563
|
+
@stream_state = snapshot[:stream_state] || StreamState.new
|
|
564
|
+
@last_composer_rows = []
|
|
565
|
+
redraw_screen_locked if @started
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def restore_composer_snapshot_locked(snapshot)
|
|
570
|
+
@composer = snapshot[:composer] || new_composer_state_with_history
|
|
571
|
+
@prompt_label = snapshot[:prompt_label].to_s.empty? ? "You>" : snapshot[:prompt_label].to_s
|
|
572
|
+
self.composer_input = @composer.input
|
|
573
|
+
self.composer_cursor = @composer.cursor
|
|
574
|
+
@last_composer_rows = []
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def new_composer_state_with_history
|
|
578
|
+
composer = ComposerState.new
|
|
579
|
+
composer.load_history(@prompt_history.values) if @prompt_history
|
|
580
|
+
composer
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def restore_editor_snapshot_locked(snapshot)
|
|
584
|
+
editor_was_active = editor_active?
|
|
585
|
+
@editor_state = snapshot[:editor_state]&.dup
|
|
586
|
+
editor_is_active = editor_active?
|
|
587
|
+
|
|
588
|
+
if editor_is_active
|
|
589
|
+
enable_editor_mouse_reporting unless editor_was_active
|
|
590
|
+
else
|
|
591
|
+
disable_editor_mouse_reporting(force: true)
|
|
592
|
+
end
|
|
346
593
|
end
|
|
347
594
|
|
|
348
595
|
def update_overlay_settings(settings)
|
|
@@ -409,6 +656,22 @@ module Kward
|
|
|
409
656
|
def poll_input
|
|
410
657
|
key = read_key(nonblock: true)
|
|
411
658
|
@mutex.synchronize do
|
|
659
|
+
if interactive_active_locked?
|
|
660
|
+
if key.nil?
|
|
661
|
+
resized = handle_resize_locked
|
|
662
|
+
ticked = tick_interactive_locked
|
|
663
|
+
render_prompt_locked if resized || ticked
|
|
664
|
+
return :interactive_exited if @interactive_state[:controller].exited?
|
|
665
|
+
return nil
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
route_interactive_key(key)
|
|
669
|
+
ticked = tick_interactive_locked
|
|
670
|
+
render_prompt_locked if ticked
|
|
671
|
+
return :interactive_exited if @interactive_state[:controller].exited?
|
|
672
|
+
return nil
|
|
673
|
+
end
|
|
674
|
+
|
|
412
675
|
if key.nil?
|
|
413
676
|
resized = handle_resize_locked
|
|
414
677
|
spun = tick_spinner_locked
|
|
@@ -417,8 +680,13 @@ module Kward
|
|
|
417
680
|
return nil
|
|
418
681
|
end
|
|
419
682
|
|
|
683
|
+
if modal_active_locked?
|
|
684
|
+
queue_pending_keys(key)
|
|
685
|
+
return nil
|
|
686
|
+
end
|
|
687
|
+
|
|
420
688
|
result = handle_key(key)
|
|
421
|
-
render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result)
|
|
689
|
+
render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result) || prompt_action_result?(result)
|
|
422
690
|
[EXIT_INPUT, CANCEL_INPUT].include?(result) ? result : result
|
|
423
691
|
end
|
|
424
692
|
end
|
|
@@ -479,6 +747,14 @@ module Kward
|
|
|
479
747
|
end
|
|
480
748
|
end
|
|
481
749
|
|
|
750
|
+
def refresh_composer_status
|
|
751
|
+
@mutex.synchronize do
|
|
752
|
+
@cached_composer_status_text = nil
|
|
753
|
+
@last_composer_status_refresh = 0.0
|
|
754
|
+
render_prompt_locked if @started && @asking
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
482
758
|
def clear_transcript
|
|
483
759
|
@mutex.synchronize do
|
|
484
760
|
@transcript_buffer.clear
|
|
@@ -493,7 +769,9 @@ module Kward
|
|
|
493
769
|
|
|
494
770
|
private
|
|
495
771
|
|
|
496
|
-
|
|
772
|
+
def modal_active_locked?
|
|
773
|
+
@question_prompt_active || !@question_state.nil? || !@select_state.nil? || !@git_state.nil?
|
|
774
|
+
end
|
|
497
775
|
|
|
498
776
|
|
|
499
777
|
|
|
@@ -25,6 +25,11 @@ module Kward
|
|
|
25
25
|
{ name: "model", description: "Select the default model.", argument_hint: "" },
|
|
26
26
|
{ name: "reasoning", description: "Select reasoning effort.", argument_hint: "" },
|
|
27
27
|
{ name: "reload", description: "Reload installed plugins.", argument_hint: "" },
|
|
28
|
+
{ name: "workers", description: "Open the worker pipeline.", argument_hint: "[new|do <task>]" },
|
|
29
|
+
{ name: "git", description: "Review uncommitted changes and commit them.", argument_hint: "" },
|
|
30
|
+
{ name: "files", description: "Browse project files.", argument_hint: "" },
|
|
31
|
+
{ name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
|
|
32
|
+
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name]" },
|
|
28
33
|
{ name: "status", description: "Show the current status message.", argument_hint: "" },
|
|
29
34
|
{ name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
|
|
30
35
|
{ name: "memory", description: "Inspect and manage Kward memory.", argument_hint: "[enable|disable|auto-summary|core|add|list|forget|promote|relax|inspect|why|summarize]" }
|
data/lib/kward/prompts.rb
CHANGED
|
@@ -35,6 +35,8 @@ module Kward
|
|
|
35
35
|
You are Kward, a concise practical CLI coding agent. Use tools to understand and modify software projects. Inspect files before changing them, make the smallest correct change, preserve existing style, and summarize what changed. Be honest about limitations.
|
|
36
36
|
|
|
37
37
|
For web research, use web_search to discover sources, fetch_content for important human-readable pages, and fetch_raw for machine-readable resources such as JSON, YAML, XML, RSS, OpenAPI specs, and plain text. Prefer official or primary sources and cite or mention the URLs you relied on.
|
|
38
|
+
|
|
39
|
+
Manage code context deliberately. Prefer context_for_task, summarize_file_structure, and read_file mode="outline"/"preview" before broad reads. Escalate to read_file mode="range" for exact lines, and use mode="full" only when focused context is insufficient. Use context_budget_stats when asked about context savings.
|
|
38
40
|
PROMPT
|
|
39
41
|
end
|
|
40
42
|
|
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -5,6 +5,7 @@ require_relative "../memory/manager"
|
|
|
5
5
|
require_relative "../plugin_registry"
|
|
6
6
|
require_relative "../prompts/commands"
|
|
7
7
|
require_relative "../tools/registry"
|
|
8
|
+
require_relative "../workers"
|
|
8
9
|
require_relative "../workspace"
|
|
9
10
|
require_relative "../telemetry/logger"
|
|
10
11
|
require_relative "../telemetry/stats"
|
|
@@ -60,14 +61,18 @@ module Kward
|
|
|
60
61
|
"memory/forget", "memory/promote", "memory/relax", "memory/inspect",
|
|
61
62
|
"memory/why", "memory/summarize"
|
|
62
63
|
].freeze
|
|
64
|
+
WORKER_METHODS = ["workers/list", "workers/show"].freeze
|
|
63
65
|
|
|
64
66
|
# Creates the RPC server and its stateful managers.
|
|
65
|
-
def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new)
|
|
67
|
+
def initialize(input: $stdin, output: $stdout, error_output: $stderr, client: Client.new, experimental_workers: false)
|
|
66
68
|
@transport = Transport.new(input: input, output: output)
|
|
67
69
|
@error_output = error_output
|
|
70
|
+
@client = client
|
|
68
71
|
@config_manager = ConfigManager.new
|
|
69
72
|
@session_manager = SessionManager.new(server: self, client: client, config_manager: @config_manager)
|
|
70
73
|
@auth_manager = AuthManager.new(server: self, config_manager: @config_manager)
|
|
74
|
+
@worker_store = Workers::Store.new
|
|
75
|
+
@experimental_workers = experimental_workers
|
|
71
76
|
@shutdown = false
|
|
72
77
|
end
|
|
73
78
|
|
|
@@ -88,7 +93,7 @@ module Kward
|
|
|
88
93
|
end
|
|
89
94
|
end
|
|
90
95
|
ensure
|
|
91
|
-
@session_manager.
|
|
96
|
+
@session_manager.shutdown_sessions
|
|
92
97
|
end
|
|
93
98
|
|
|
94
99
|
# Sends a redacted JSON-RPC notification to the client.
|
|
@@ -222,6 +227,12 @@ module Kward
|
|
|
222
227
|
@session_manager.memory_why(session_id: params["sessionId"])
|
|
223
228
|
when "memory/summarize"
|
|
224
229
|
@session_manager.memory_summarize(session_id: params.fetch("sessionId"))
|
|
230
|
+
when "workers/list"
|
|
231
|
+
require_experimental_workers!
|
|
232
|
+
workers_list(params)
|
|
233
|
+
when "workers/show"
|
|
234
|
+
require_experimental_workers!
|
|
235
|
+
workers_show(params)
|
|
225
236
|
when "auth/status"
|
|
226
237
|
@auth_manager.status
|
|
227
238
|
when "auth/providers"
|
|
@@ -343,7 +354,7 @@ module Kward
|
|
|
343
354
|
reasoning: { start: false, delta: true, end: false },
|
|
344
355
|
modelRetry: { supported: true, event: "modelRetry" },
|
|
345
356
|
steering: { supported: @session_manager.in_flight_steer_supported?, event: "turnSteered", mode: @session_manager.in_flight_steer_supported? ? "native" : "unsupported" },
|
|
346
|
-
tools: { call: true, update: false, result: true, normalizedMetadata: true, diffs: true, changedFiles: false, workspaceGuardrails: workspace_guardrails_enabled
|
|
357
|
+
tools: { call: true, update: false, result: true, normalizedMetadata: true, diffs: true, changedFiles: false, workspaceGuardrails: workspace_guardrails_enabled?, focusedContext: true, contextBudgetStats: true },
|
|
347
358
|
errors: true,
|
|
348
359
|
sessionUpdates: false
|
|
349
360
|
},
|
|
@@ -384,9 +395,11 @@ module Kward
|
|
|
384
395
|
logout: true
|
|
385
396
|
},
|
|
386
397
|
memory: { supported: true, optIn: true, defaultEnabled: false, autoSummaryDefaultEnabled: false, promptInjection: "interactive", storage: { core: "json", soft: "jsonl", events: "jsonl" }, methods: MEMORY_METHODS },
|
|
398
|
+
workers: workers_capability,
|
|
387
399
|
commands: { supported: true, methods: ["commands/list", "commands/run"], method: "commands/list", runMethod: "commands/run", sources: ["builtin", "prompt", "skill", "plugin"], executableSources: ["builtin", "plugin"] },
|
|
388
400
|
startupResources: { supported: true, method: "resources/startup" },
|
|
389
401
|
starterPack: { supported: false, reason: "cliOnlyInstallCommand" },
|
|
402
|
+
shell: { supported: false, reason: "interactiveTuiOnly" },
|
|
390
403
|
extensionUi: {
|
|
391
404
|
question: { supported: true, notification: "ui/question", method: "ui/answerQuestion", maxQuestions: 4, multiSelect: false, preview: false },
|
|
392
405
|
select: false,
|
|
@@ -426,6 +439,18 @@ module Kward
|
|
|
426
439
|
}
|
|
427
440
|
end
|
|
428
441
|
|
|
442
|
+
def workers_capability
|
|
443
|
+
return { supported: false, reason: "experimentalWorkersFlagRequired", flag: "--experimental-workers" } unless @experimental_workers
|
|
444
|
+
|
|
445
|
+
{ supported: true, methods: WORKER_METHODS, roles: ["implementation", "request"], statuses: Workers::Worker::STATUSES, transcriptStorage: "sessions", metadataStorage: "json" }
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def require_experimental_workers!
|
|
449
|
+
return if @experimental_workers
|
|
450
|
+
|
|
451
|
+
raise NoMethodError, "workers require --experimental-workers"
|
|
452
|
+
end
|
|
453
|
+
|
|
429
454
|
def workspace_info(root)
|
|
430
455
|
root = @session_manager.validate_workspace_root(root)
|
|
431
456
|
{ root: root, basename: File.basename(root), writable: File.writable?(root) }
|
|
@@ -571,6 +596,20 @@ module Kward
|
|
|
571
596
|
{ sections: sections }
|
|
572
597
|
end
|
|
573
598
|
|
|
599
|
+
def workers_list(params)
|
|
600
|
+
include_archived = params["includeArchived"] == true
|
|
601
|
+
workers = @worker_store.list(include_archived: include_archived)
|
|
602
|
+
{ workers: workers }
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
def workers_show(params)
|
|
606
|
+
id = params.fetch("id").to_s.delete_prefix("#")
|
|
607
|
+
worker = @worker_store.find(id)
|
|
608
|
+
return { worker: worker } if worker
|
|
609
|
+
|
|
610
|
+
raise ArgumentError, "Unknown worker: #{id}"
|
|
611
|
+
end
|
|
612
|
+
|
|
574
613
|
def auth_login_with_api_key(params)
|
|
575
614
|
result = @auth_manager.login_with_api_key(provider_id: params.fetch("providerId"), api_key: params.fetch("apiKey"))
|
|
576
615
|
@session_manager.refresh_client_config
|