kward 0.82.0 → 0.84.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +96 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/agent-tools.md +1 -0
- data/doc/api.md +4 -0
- data/doc/composer.md +2 -2
- data/doc/configuration.md +82 -20
- data/doc/editor.md +47 -22
- data/doc/files.md +12 -7
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/permissions.md +1 -0
- data/doc/platform-support.md +48 -0
- data/doc/releasing.md +12 -4
- data/doc/rpc.md +2 -2
- data/doc/sandboxing.md +5 -1
- data/doc/security.md +4 -3
- data/doc/shell.md +101 -60
- data/doc/tabs.md +3 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +7 -6
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +7 -3
- data/lib/kward/ansi.rb +110 -10
- data/lib/kward/auth/anthropic_oauth.rb +7 -7
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +144 -21
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +27 -0
- data/lib/kward/cli/rendering.rb +15 -9
- data/lib/kward/cli/runtime_helpers.rb +260 -55
- data/lib/kward/cli/slash_commands.rb +21 -12
- data/lib/kward/cli/tabs.rb +91 -13
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +55 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/compaction/token_estimator.rb +12 -6
- data/lib/kward/config_files.rb +94 -63
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/editor_prompt.rb +46 -0
- data/lib/kward/editor_prompt_session.rb +28 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +103 -24
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +35 -39
- data/lib/kward/model/model_info.rb +3 -2
- data/lib/kward/model/payloads.rb +0 -2
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +23 -3
- data/lib/kward/persistent_shell_session.rb +843 -0
- data/lib/kward/project_files.rb +19 -6
- data/lib/kward/prompt_interface/composer_renderer.rb +45 -41
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +27 -22
- data/lib/kward/prompt_interface/editor/controller.rb +99 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/modern.rb +4 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +298 -47
- data/lib/kward/prompt_interface/editor/renderer.rb +109 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/state.rb +21 -10
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/editor/word_completion.rb +124 -0
- data/lib/kward/prompt_interface/file_overlay.rb +92 -22
- data/lib/kward/prompt_interface/key_handler.rb +75 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +443 -17
- data/lib/kward/prompt_interface/runtime_state.rb +55 -2
- data/lib/kward/prompt_interface/screen.rb +2 -2
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +169 -18
- data/lib/kward/prompts/commands.rb +2 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- data/lib/kward/rpc/auth_manager.rb +1 -1
- data/lib/kward/rpc/server.rb +2 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/scratchpad_languages.rb +74 -0
- data/lib/kward/scratchpad_runner.rb +155 -29
- data/lib/kward/shell_prompt.rb +52 -0
- data/lib/kward/shell_prompt_session.rb +58 -0
- data/lib/kward/terminal_keys.rb +13 -0
- data/lib/kward/terminal_text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +28 -0
- data/lib/kward/tools/registry.rb +75 -8
- data/lib/kward/tools/replace_editor_buffer.rb +30 -0
- data/lib/kward/tools/run_shell_command.rb +24 -6
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/version.rb +1 -1
- data/templates/default/fulldoc/html/css/kward.css +0 -125
- data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +6 -18
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +44 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
|
@@ -338,6 +338,7 @@ module Kward
|
|
|
338
338
|
|
|
339
339
|
@mutex.synchronize do
|
|
340
340
|
@busy = true
|
|
341
|
+
@busy_started_at = monotonic_now
|
|
341
342
|
@busy_activity = normalize_busy_activity(activity)
|
|
342
343
|
@asking = true
|
|
343
344
|
reset_spinner_locked
|
|
@@ -349,7 +350,8 @@ module Kward
|
|
|
349
350
|
def finish_select_action
|
|
350
351
|
@mutex.synchronize do
|
|
351
352
|
@busy = false
|
|
352
|
-
@
|
|
353
|
+
@busy_started_at = nil
|
|
354
|
+
@busy_activity = "thinking"
|
|
353
355
|
@select_state&.delete(:busy_activity)
|
|
354
356
|
render_prompt_locked if @asking
|
|
355
357
|
flush_output_locked
|
|
@@ -19,13 +19,17 @@ module Kward
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def normalize_slash_commands(commands)
|
|
22
|
-
commands.
|
|
22
|
+
commands.filter_map do |command|
|
|
23
|
+
name = slash_command_value(command, :name).to_s
|
|
24
|
+
next if name.empty?
|
|
25
|
+
|
|
23
26
|
{
|
|
24
|
-
name:
|
|
27
|
+
name: name,
|
|
28
|
+
normalized_name: name.downcase,
|
|
25
29
|
description: slash_command_value(command, :description).to_s,
|
|
26
30
|
argument_hint: slash_command_value(command, :argument_hint).to_s
|
|
27
31
|
}
|
|
28
|
-
end.
|
|
32
|
+
end.sort_by { |command| command[:name] }
|
|
29
33
|
end
|
|
30
34
|
|
|
31
35
|
def slash_command_value(command, key)
|
|
@@ -44,7 +48,18 @@ module Kward
|
|
|
44
48
|
|
|
45
49
|
def slash_overlay_matches
|
|
46
50
|
prefix = composer_input.delete_prefix("/").downcase
|
|
47
|
-
|
|
51
|
+
cache = @slash_overlay_match_cache
|
|
52
|
+
return cache[:matches] if cache && cache[:prefix] == prefix && cache[:commands].equal?(@slash_commands)
|
|
53
|
+
|
|
54
|
+
matches = []
|
|
55
|
+
@slash_commands.each do |command|
|
|
56
|
+
next unless command[:normalized_name].start_with?(prefix)
|
|
57
|
+
|
|
58
|
+
matches << command
|
|
59
|
+
break if matches.length == 8
|
|
60
|
+
end
|
|
61
|
+
@slash_overlay_match_cache = { prefix: prefix, commands: @slash_commands, matches: matches }
|
|
62
|
+
matches
|
|
48
63
|
end
|
|
49
64
|
|
|
50
65
|
def selected_slash_command
|
|
@@ -11,7 +11,7 @@ module Kward
|
|
|
11
11
|
prepare_transcript_output_locked unless @restoring_transcript
|
|
12
12
|
if label && @stream_state.block != label
|
|
13
13
|
ensure_transcript_block_separator_locked
|
|
14
|
-
write_transcript_text_locked("#{colored(
|
|
14
|
+
write_transcript_text_locked("#{colored(transcript_marker(label), *label_styles(label))} ")
|
|
15
15
|
@stream_state.start_block(label)
|
|
16
16
|
end
|
|
17
17
|
write_transcript_text_locked(delta) unless delta.empty?
|
|
@@ -117,20 +117,25 @@ module Kward
|
|
|
117
117
|
end
|
|
118
118
|
end
|
|
119
119
|
|
|
120
|
+
def transcript_marker(label)
|
|
121
|
+
marker = "#{transcript_label(label)}>"
|
|
122
|
+
label == "Assistant" ? "✦ #{marker}" : marker
|
|
123
|
+
end
|
|
124
|
+
|
|
120
125
|
def label_styles(label)
|
|
121
126
|
case label
|
|
122
127
|
when "Reasoning", "Compaction summary"
|
|
123
|
-
[:
|
|
128
|
+
[:metadata, :bold]
|
|
124
129
|
when "Assistant", "Kward"
|
|
125
|
-
[:
|
|
130
|
+
[:activity, :bold]
|
|
126
131
|
when "Tool", "Tool output"
|
|
127
|
-
[:
|
|
132
|
+
[:tool, :bold]
|
|
128
133
|
when "Tool failed"
|
|
129
|
-
[:
|
|
134
|
+
[:failure, :bold]
|
|
130
135
|
when "Retry"
|
|
131
|
-
[:
|
|
136
|
+
[:caution, :bold]
|
|
132
137
|
else
|
|
133
|
-
[:
|
|
138
|
+
[:metadata, :bold]
|
|
134
139
|
end
|
|
135
140
|
end
|
|
136
141
|
|
|
@@ -15,6 +15,8 @@ require_relative "terminal_sequences"
|
|
|
15
15
|
require_relative "terminal_keys"
|
|
16
16
|
require_relative "editor_mode"
|
|
17
17
|
require_relative "diff_view_mode"
|
|
18
|
+
require_relative "scratchpad_languages"
|
|
19
|
+
require_relative "markdown_code_block"
|
|
18
20
|
require_relative "prompt_interface/banner"
|
|
19
21
|
require_relative "prompt_interface/composer_state"
|
|
20
22
|
require_relative "prompt_interface/editor/state"
|
|
@@ -32,8 +34,10 @@ require_relative "prompt_interface/git_prompt"
|
|
|
32
34
|
require_relative "prompt_interface/overlay_renderer"
|
|
33
35
|
require_relative "prompt_interface/editor/renderer"
|
|
34
36
|
require_relative "prompt_interface/editor/syntax_highlighter"
|
|
37
|
+
require_relative "prompt_interface/editor/runner"
|
|
35
38
|
require_relative "prompt_interface/editor/auto_close_pairs"
|
|
36
39
|
require_relative "prompt_interface/editor/endwise"
|
|
40
|
+
require_relative "prompt_interface/editor/word_completion"
|
|
37
41
|
require_relative "prompt_interface/editor/auto_indent"
|
|
38
42
|
require_relative "prompt_interface/composer_renderer"
|
|
39
43
|
require_relative "prompt_interface/composer_controller"
|
|
@@ -63,12 +67,15 @@ module Kward
|
|
|
63
67
|
# `prompt_interface/` to keep rendering, layout, keyboard handling, and runtime
|
|
64
68
|
# state readable while sharing one terminal state object.
|
|
65
69
|
class PromptInterface
|
|
66
|
-
HELP_TEXT = "Enter sends • Shift+Enter
|
|
70
|
+
HELP_TEXT = "Enter sends • Shift+Enter newline • / commands • @ files • ↑/↓ history • Ctrl+D exits".freeze
|
|
67
71
|
BUSY_HELP_TEXT = "Ctrl+C cancels".freeze
|
|
68
|
-
SPINNER_FRAMES = %w[
|
|
69
|
-
SPINNER_INTERVAL = 0.
|
|
72
|
+
SPINNER_FRAMES = %w[· • ● •].freeze
|
|
73
|
+
SPINNER_INTERVAL = 0.16
|
|
70
74
|
FOOTER_REFRESH_INTERVAL = 1.0
|
|
71
75
|
COMPOSER_STATUS_REFRESH_INTERVAL = 1.0
|
|
76
|
+
COMPLETION_DISPLAY_SECONDS = 0.8
|
|
77
|
+
RESPONSE_ARRIVAL_SECONDS = 0.4
|
|
78
|
+
BUSY_ELAPSED_DELAY_SECONDS = 2.0
|
|
72
79
|
COMPOSER_MAX_INPUT_ROWS = 6
|
|
73
80
|
IMAGE_VIEWER_MAX_ROWS = 8
|
|
74
81
|
TRANSCRIPT_BUFFER_LIMIT = 200_000
|
|
@@ -86,7 +93,9 @@ module Kward
|
|
|
86
93
|
include EditorSyntaxHighlighter
|
|
87
94
|
include EditorAutoClosePairs
|
|
88
95
|
include EditorEndwise
|
|
96
|
+
include EditorWordCompletion
|
|
89
97
|
include EditorAutoIndent
|
|
98
|
+
include EditorRunner
|
|
90
99
|
include ComposerRenderer
|
|
91
100
|
include ComposerController
|
|
92
101
|
include ModernEditorMode
|
|
@@ -131,7 +140,7 @@ module Kward
|
|
|
131
140
|
end
|
|
132
141
|
end
|
|
133
142
|
|
|
134
|
-
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, project_browser_icon_theme: "off", footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, workspace_root: Dir.pwd, 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, redraw_handler: nil)
|
|
143
|
+
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, project_browser_icon_theme: "off", footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, workspace_root: Dir.pwd, 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, editor_runners_source: nil, redraw_handler: nil)
|
|
135
144
|
@input_io = input
|
|
136
145
|
@output_io = output
|
|
137
146
|
# Logical state may change during a child handoff, but only the owner may render.
|
|
@@ -143,12 +152,14 @@ module Kward
|
|
|
143
152
|
@workspace_root = File.expand_path(workspace_root.to_s.empty? ? Dir.pwd : workspace_root)
|
|
144
153
|
@composer = ComposerState.new
|
|
145
154
|
load_history(@prompt_history.values) if @prompt_history
|
|
146
|
-
self.composer_input = @composer.input
|
|
147
|
-
self.composer_cursor = @composer.cursor
|
|
148
155
|
@started = false
|
|
149
156
|
@asking = false
|
|
150
157
|
@busy = false
|
|
151
|
-
@busy_activity = "
|
|
158
|
+
@busy_activity = "thinking"
|
|
159
|
+
@busy_started_at = nil
|
|
160
|
+
@last_busy_elapsed = nil
|
|
161
|
+
@completion_status = nil
|
|
162
|
+
@response_arrival_until = nil
|
|
152
163
|
@queued_count = 0
|
|
153
164
|
@steered_count = 0
|
|
154
165
|
@spinner_frame_index = 0
|
|
@@ -167,6 +178,7 @@ module Kward
|
|
|
167
178
|
@transcript_viewport_rows = 0
|
|
168
179
|
@restoring_transcript = false
|
|
169
180
|
@pending_keys = []
|
|
181
|
+
@handoff_input_buffer = +"".b
|
|
170
182
|
@completion_provider = nil
|
|
171
183
|
@completion_cycle = nil
|
|
172
184
|
@completion_overlay = nil
|
|
@@ -183,11 +195,25 @@ module Kward
|
|
|
183
195
|
@file_open_dismissed_token = nil
|
|
184
196
|
@file_editor_open_status = nil
|
|
185
197
|
@file_mention_paths = nil
|
|
198
|
+
@file_mention_discovery_thread = nil
|
|
199
|
+
@file_mention_discovery_root = nil
|
|
200
|
+
@file_mention_discovery_token = nil
|
|
201
|
+
@project_browser_file_paths = nil
|
|
186
202
|
@project_browser_state = nil
|
|
187
203
|
@image_viewer_state = nil
|
|
188
204
|
@terminal_image_protocol = nil
|
|
189
205
|
@project_browser_restore_after_editor = false
|
|
190
206
|
@editor_state = nil
|
|
207
|
+
@editor_runner_state = nil
|
|
208
|
+
@editor_runner_completion = nil
|
|
209
|
+
@editor_runner_output_visible = false
|
|
210
|
+
@editor_runner_selection_anchor = nil
|
|
211
|
+
@editor_runner_selection_cursor = nil
|
|
212
|
+
@editor_runner_mouse_dragging = false
|
|
213
|
+
@editor_runner_last_click = nil
|
|
214
|
+
@editor_agent_suspended = false
|
|
215
|
+
@editor_prompt_active = false
|
|
216
|
+
@editor_prompt_input = ""
|
|
191
217
|
@interactive_state = nil
|
|
192
218
|
@last_interactive_tick = monotonic_now
|
|
193
219
|
@select_state = nil
|
|
@@ -226,6 +252,7 @@ module Kward
|
|
|
226
252
|
@editor_line_numbers_source = editor_line_numbers_source
|
|
227
253
|
@diff_view = DiffViewMode.normalize(diff_view)
|
|
228
254
|
@diff_view_source = diff_view_source
|
|
255
|
+
@editor_runners_source = editor_runners_source
|
|
229
256
|
end
|
|
230
257
|
|
|
231
258
|
def update_slash_commands(commands)
|
|
@@ -352,6 +379,60 @@ module Kward
|
|
|
352
379
|
@mutex.synchronize { editor_active? }
|
|
353
380
|
end
|
|
354
381
|
|
|
382
|
+
# Returns the active Vibe or Modern editor buffer for an agent prompt turn.
|
|
383
|
+
def editor_prompt_context
|
|
384
|
+
@mutex.synchronize do
|
|
385
|
+
next nil unless editor_active? && (@editor_state.vibe? || @editor_state.modern?)
|
|
386
|
+
|
|
387
|
+
{
|
|
388
|
+
path: @editor_state.path,
|
|
389
|
+
display_path: @editor_state.display_path,
|
|
390
|
+
workspace_root: @workspace_root,
|
|
391
|
+
language: @editor_state.language,
|
|
392
|
+
content: @editor_state.buffer.dup,
|
|
393
|
+
cursor: @editor_state.cursor_line_and_column,
|
|
394
|
+
selection: @editor_state.selected_text
|
|
395
|
+
}
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
# Suspends the live editor while an agent prompt is running.
|
|
400
|
+
def suspend_editor_for_agent
|
|
401
|
+
@mutex.synchronize do
|
|
402
|
+
return false unless editor_active? && !@editor_agent_suspended
|
|
403
|
+
|
|
404
|
+
@editor_agent_suspended = true
|
|
405
|
+
@editor_state.status = "Agent is working · Ctrl+C cancels"
|
|
406
|
+
render_prompt_locked if @started
|
|
407
|
+
true
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Commits an agent draft as one undoable editor operation.
|
|
412
|
+
def commit_editor_prompt(session)
|
|
413
|
+
@mutex.synchronize do
|
|
414
|
+
return false unless editor_active? && @editor_agent_suspended
|
|
415
|
+
return false unless @editor_state.buffer == session.initial_content
|
|
416
|
+
|
|
417
|
+
changed = @editor_state.replace_buffer(session.content)
|
|
418
|
+
@editor_state.status = changed ? "Agent updated buffer · :w save" : "Agent made no buffer changes"
|
|
419
|
+
render_prompt_locked if @started
|
|
420
|
+
changed
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Resumes the editor after an agent prompt turn.
|
|
425
|
+
def resume_editor_for_agent(status: nil)
|
|
426
|
+
@mutex.synchronize do
|
|
427
|
+
return false unless editor_active?
|
|
428
|
+
|
|
429
|
+
@editor_agent_suspended = false
|
|
430
|
+
@editor_state.status = status.to_s unless status.to_s.empty?
|
|
431
|
+
render_prompt_locked if @started
|
|
432
|
+
true
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
355
436
|
def inline_image_protocol
|
|
356
437
|
@mutex.synchronize { terminal_image_protocol }
|
|
357
438
|
end
|
|
@@ -379,6 +460,12 @@ module Kward
|
|
|
379
460
|
return false unless opened
|
|
380
461
|
|
|
381
462
|
run_editor
|
|
463
|
+
rescue StandardError
|
|
464
|
+
@mutex.synchronize do
|
|
465
|
+
close_editor if editor_active?
|
|
466
|
+
render_prompt_locked if @started
|
|
467
|
+
end
|
|
468
|
+
raise
|
|
382
469
|
end
|
|
383
470
|
|
|
384
471
|
def scratchpad(language = :text)
|
|
@@ -464,7 +551,8 @@ module Kward
|
|
|
464
551
|
if key.nil?
|
|
465
552
|
resized = handle_resize_locked
|
|
466
553
|
footer_refreshed = tick_footer_locked
|
|
467
|
-
|
|
554
|
+
completion_changed = tick_completion_locked
|
|
555
|
+
render_prompt_locked if resized || footer_refreshed || completion_changed
|
|
468
556
|
else
|
|
469
557
|
result = handle_key(key)
|
|
470
558
|
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
|
|
@@ -480,6 +568,21 @@ module Kward
|
|
|
480
568
|
perform_pending_transcript_redraw
|
|
481
569
|
end
|
|
482
570
|
|
|
571
|
+
# Prefills the next composer input without submitting it.
|
|
572
|
+
def prefill_input(value)
|
|
573
|
+
@mutex.synchronize do
|
|
574
|
+
@composer.prefill_input = value.to_s
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
# Updates the visible prompt label while the composer is active.
|
|
579
|
+
def update_prompt_label(label)
|
|
580
|
+
@mutex.synchronize do
|
|
581
|
+
@prompt_label = label.to_s
|
|
582
|
+
render_prompt_locked if @started && @asking
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
483
586
|
def yes?(message, default: false)
|
|
484
587
|
answer = ask("#{message} #{default ? "[Y/n]" : "[y/N]"}")
|
|
485
588
|
return default if answer.nil?
|
|
@@ -592,11 +695,11 @@ module Kward
|
|
|
592
695
|
end
|
|
593
696
|
end
|
|
594
697
|
|
|
595
|
-
def with_inline_terminal_handoff(&block)
|
|
596
|
-
with_terminal_handoff(inline: true, &block)
|
|
698
|
+
def with_inline_terminal_handoff(preserve_tab_keybindings: false, &block)
|
|
699
|
+
with_terminal_handoff(inline: true, preserve_tab_keybindings: preserve_tab_keybindings, &block)
|
|
597
700
|
end
|
|
598
701
|
|
|
599
|
-
def with_terminal_handoff(inline: false)
|
|
702
|
+
def with_terminal_handoff(inline: false, preserve_tab_keybindings: false)
|
|
600
703
|
start
|
|
601
704
|
input = nil
|
|
602
705
|
output = nil
|
|
@@ -612,7 +715,8 @@ module Kward
|
|
|
612
715
|
end
|
|
613
716
|
disable_editor_mouse_reporting(force: true)
|
|
614
717
|
print_output_locked(BRACKETED_PASTE_RESTORE)
|
|
615
|
-
print_output_locked(KEYBOARD_PROTOCOL_RESTORE)
|
|
718
|
+
print_output_locked(KEYBOARD_PROTOCOL_RESTORE) unless preserve_tab_keybindings
|
|
719
|
+
@handoff_input_buffer.clear
|
|
616
720
|
restore_editor_cursor_shape_locked
|
|
617
721
|
set_cursor_visible_locked(true, force: true)
|
|
618
722
|
flush_output_locked
|
|
@@ -634,6 +738,7 @@ module Kward
|
|
|
634
738
|
enter_raw_mode_locked
|
|
635
739
|
print_output_locked(KEYBOARD_PROTOCOL_ENABLE)
|
|
636
740
|
print_output_locked(BRACKETED_PASTE_ENABLE)
|
|
741
|
+
@handoff_input_buffer.clear
|
|
637
742
|
disable_editor_mouse_reporting(force: true)
|
|
638
743
|
restore_editor_cursor_shape_locked(force: true)
|
|
639
744
|
@cursor_visible = nil
|
|
@@ -727,8 +832,6 @@ module Kward
|
|
|
727
832
|
@completion_cycle = nil
|
|
728
833
|
@completion_overlay = nil
|
|
729
834
|
@prompt_label = snapshot[:prompt_label].to_s.empty? ? "You>" : snapshot[:prompt_label].to_s
|
|
730
|
-
self.composer_input = @composer.input
|
|
731
|
-
self.composer_cursor = @composer.cursor
|
|
732
835
|
@last_composer_rows = []
|
|
733
836
|
end
|
|
734
837
|
|
|
@@ -740,6 +843,11 @@ module Kward
|
|
|
740
843
|
|
|
741
844
|
def restore_editor_snapshot_locked(snapshot)
|
|
742
845
|
editor_was_active = editor_active?
|
|
846
|
+
cancel_editor_runner
|
|
847
|
+
@editor_runner_state = nil
|
|
848
|
+
@editor_runner_completion = nil
|
|
849
|
+
@editor_runner_output_visible = false
|
|
850
|
+
clear_editor_runner_selection
|
|
743
851
|
@editor_state = snapshot[:editor_state]&.dup
|
|
744
852
|
editor_is_active = editor_active?
|
|
745
853
|
|
|
@@ -764,17 +872,21 @@ module Kward
|
|
|
764
872
|
end
|
|
765
873
|
end
|
|
766
874
|
|
|
767
|
-
def begin_busy_input(message = "You>", activity: "
|
|
875
|
+
def begin_busy_input(message = "You>", activity: "thinking")
|
|
768
876
|
start
|
|
769
877
|
@mutex.synchronize do
|
|
770
878
|
@prompt_label = message.to_s
|
|
771
879
|
@busy_activity = normalize_busy_activity(activity)
|
|
880
|
+
@completion_status = nil
|
|
881
|
+
@response_arrival_until = nil
|
|
772
882
|
self.composer_input = ""
|
|
773
883
|
self.composer_cursor = 0
|
|
774
884
|
@composer.clear_attachments
|
|
775
885
|
@pending_keys.clear
|
|
776
886
|
@asking = true
|
|
777
887
|
@busy = true
|
|
888
|
+
@busy_started_at = monotonic_now
|
|
889
|
+
@last_busy_elapsed = nil
|
|
778
890
|
@queued_count = 0
|
|
779
891
|
@steered_count = 0
|
|
780
892
|
reset_spinner_locked
|
|
@@ -783,6 +895,17 @@ module Kward
|
|
|
783
895
|
end
|
|
784
896
|
end
|
|
785
897
|
|
|
898
|
+
def update_busy_activity(activity)
|
|
899
|
+
@mutex.synchronize do
|
|
900
|
+
value = normalize_busy_activity(activity)
|
|
901
|
+
return false if value == @busy_activity
|
|
902
|
+
|
|
903
|
+
@busy_activity = value
|
|
904
|
+
render_prompt_locked if @started && @asking && @busy
|
|
905
|
+
true
|
|
906
|
+
end
|
|
907
|
+
end
|
|
908
|
+
|
|
786
909
|
def set_queued_count(count)
|
|
787
910
|
@mutex.synchronize do
|
|
788
911
|
@queued_count = count.to_i
|
|
@@ -802,15 +925,18 @@ module Kward
|
|
|
802
925
|
def clear_steered_count
|
|
803
926
|
@mutex.synchronize do
|
|
804
927
|
@steered_count = 0
|
|
805
|
-
@busy_activity = "
|
|
928
|
+
@busy_activity = "thinking"
|
|
806
929
|
render_prompt_locked if @asking
|
|
807
930
|
end
|
|
808
931
|
end
|
|
809
932
|
|
|
810
933
|
def finish_busy_input
|
|
811
934
|
@mutex.synchronize do
|
|
935
|
+
@last_busy_elapsed = monotonic_now - @busy_started_at if @busy_started_at
|
|
936
|
+
@busy_started_at = nil
|
|
812
937
|
@busy = false
|
|
813
|
-
@busy_activity = "
|
|
938
|
+
@busy_activity = "thinking"
|
|
939
|
+
@response_arrival_until = nil
|
|
814
940
|
@queued_count = 0
|
|
815
941
|
@steered_count = 0
|
|
816
942
|
@asking = true
|
|
@@ -818,6 +944,24 @@ module Kward
|
|
|
818
944
|
end
|
|
819
945
|
end
|
|
820
946
|
|
|
947
|
+
def show_response_arrival
|
|
948
|
+
@mutex.synchronize do
|
|
949
|
+
@response_arrival_until = monotonic_now + RESPONSE_ARRIVAL_SECONDS
|
|
950
|
+
render_prompt_locked if @started && @asking && @busy
|
|
951
|
+
end
|
|
952
|
+
end
|
|
953
|
+
|
|
954
|
+
def show_completion(status)
|
|
955
|
+
@mutex.synchronize do
|
|
956
|
+
@completion_status = completion_status(status)
|
|
957
|
+
render_prompt_locked if @started && @asking
|
|
958
|
+
end
|
|
959
|
+
end
|
|
960
|
+
|
|
961
|
+
def filter_terminal_handoff_input(chunk)
|
|
962
|
+
@mutex.synchronize { filter_terminal_handoff_input_locked(chunk) }
|
|
963
|
+
end
|
|
964
|
+
|
|
821
965
|
def poll_input
|
|
822
966
|
key = read_key(nonblock: true)
|
|
823
967
|
@mutex.synchronize do
|
|
@@ -841,7 +985,8 @@ module Kward
|
|
|
841
985
|
resized = handle_resize_locked
|
|
842
986
|
spun = tick_spinner_locked
|
|
843
987
|
footer_refreshed = tick_footer_locked
|
|
844
|
-
|
|
988
|
+
completion_changed = tick_completion_locked
|
|
989
|
+
render_prompt_locked if resized || spun || footer_refreshed || completion_changed
|
|
845
990
|
return nil
|
|
846
991
|
end
|
|
847
992
|
|
|
@@ -1031,6 +1176,12 @@ module Kward
|
|
|
1031
1176
|
|
|
1032
1177
|
def reset_workspace_file_state_locked
|
|
1033
1178
|
@file_mention_paths = nil
|
|
1179
|
+
@file_mention_discovery_thread = nil
|
|
1180
|
+
@file_mention_discovery_root = nil
|
|
1181
|
+
@file_mention_discovery_token = nil
|
|
1182
|
+
@file_overlay_match_cache = nil
|
|
1183
|
+
@project_browser_search_cache = nil
|
|
1184
|
+
@project_browser_file_paths = nil
|
|
1034
1185
|
@file_mention_path_entries_paths = nil
|
|
1035
1186
|
@file_mention_path_entries = nil
|
|
1036
1187
|
@project_browser_state = nil
|
|
@@ -29,9 +29,10 @@ module Kward
|
|
|
29
29
|
{ name: "files", description: "Browse project files.", argument_hint: "" },
|
|
30
30
|
{ name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
|
|
31
31
|
{ name: "capture", description: "Run a command with bounded transcript capture.", argument_hint: "<command>" },
|
|
32
|
-
{ name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[
|
|
32
|
+
{ name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[language|help]" },
|
|
33
33
|
{ name: "pty", description: "Run a command in an interactive PTY passthrough session.", argument_hint: "<command>" },
|
|
34
34
|
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name|worktree]" },
|
|
35
|
+
{ name: "worktree", description: "Manage the active tab's linked Git worktree.", argument_hint: "[activate|detach|status|merge|merge abort|remove]" },
|
|
35
36
|
{ name: "status", description: "Show the current status message.", argument_hint: "" },
|
|
36
37
|
{ name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
|
|
37
38
|
{ name: "sandbox", description: "Inspect or configure model command sandboxing.", argument_hint: "[status|off|read_only|workspace_write|network allow|network deny]" },
|
data/lib/kward/prompts.rb
CHANGED
|
@@ -60,7 +60,7 @@ module Kward
|
|
|
60
60
|
|
|
61
61
|
When web tools are available, 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. Use code_search for package, GitHub repository, and source-code research.
|
|
62
62
|
|
|
63
|
-
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. If output is compacted or replaced with a duplicate reference, use retrieve_tool_output to inspect the needed original detail. Use context_budget_stats when asked about context savings. When a material requirement is ambiguous, use ask_user_question when it is available; otherwise state the assumption.
|
|
63
|
+
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. If output is compacted or replaced with a duplicate reference, use retrieve_tool_output to inspect the needed original detail. Use context_budget_stats when asked about context savings. When the user asks to open a file in the built-in editor and open_editor is advertised, use open_editor rather than read_file; use read_file when they ask you to inspect or display contents. Opening the editor is a user-facing action and does not itself modify or save the file. When a material requirement is ambiguous, use ask_user_question when it is available; otherwise state the assumption.
|
|
64
64
|
PROMPT
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -1,5 +1,52 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
1
3
|
# Namespace for the Kward CLI agent runtime.
|
|
2
4
|
module Kward
|
|
5
|
+
# Retains output while a detached PTY command runs without terminal ownership.
|
|
6
|
+
class BufferedPtyOutputSink
|
|
7
|
+
attr_reader :captured_output
|
|
8
|
+
|
|
9
|
+
def initialize(max_capture_bytes:)
|
|
10
|
+
@max_capture_bytes = max_capture_bytes
|
|
11
|
+
@captured_output = +"".b
|
|
12
|
+
@truncated = false
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def write(chunk)
|
|
17
|
+
value = chunk.to_s.b
|
|
18
|
+
@mutex.synchronize do
|
|
19
|
+
remaining = @max_capture_bytes - @captured_output.bytesize
|
|
20
|
+
if value.bytesize > remaining
|
|
21
|
+
@captured_output << value.byteslice(0, remaining) if remaining.positive?
|
|
22
|
+
@truncated = true
|
|
23
|
+
else
|
|
24
|
+
@captured_output << value
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def flush
|
|
30
|
+
nil
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def finish
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def transcript_safe?
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def pre_input_capture_only?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def truncated?
|
|
46
|
+
@mutex.synchronize { @truncated }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
3
50
|
# Forwards PTY output immediately and optionally keeps a bounded byte copy.
|
|
4
51
|
#
|
|
5
52
|
# The runner only depends on the sink's `write` method and optional `flush`
|
|
@@ -235,7 +235,7 @@ module Kward
|
|
|
235
235
|
def complete_login(login, code)
|
|
236
236
|
raise "Missing authorization code" if code.to_s.empty?
|
|
237
237
|
|
|
238
|
-
login.oauth.complete_login_flow(code: code, redirect_uri: login.redirect_uri, code_verifier: login.pkce[:verifier])
|
|
238
|
+
login.oauth.complete_login_flow(code: code, redirect_uri: login.redirect_uri, code_verifier: login.pkce[:verifier], state: login.state)
|
|
239
239
|
login.status = "completed"
|
|
240
240
|
@server.notify("auth/loginFinished", login_payload(login))
|
|
241
241
|
rescue StandardError => e
|
data/lib/kward/rpc/server.rb
CHANGED
|
@@ -610,7 +610,8 @@ module Kward
|
|
|
610
610
|
},
|
|
611
611
|
composer: {
|
|
612
612
|
sessionDiff: { supported: false, reason: "interactiveComposerOnly" },
|
|
613
|
-
copy: { supported: false, reason: "clientClipboardOwnedByUi" }
|
|
613
|
+
copy: { supported: false, reason: "clientClipboardOwnedByUi" },
|
|
614
|
+
editorPrompt: { supported: false, reason: "interactiveVibeEditorOnly" }
|
|
614
615
|
},
|
|
615
616
|
security: {
|
|
616
617
|
workspaceMutationGuard: "none",
|
|
@@ -173,10 +173,14 @@ module Kward
|
|
|
173
173
|
when "reasoning"
|
|
174
174
|
reasoning_item_blocks(item).map { |thinking| { type: "thinking", thinking: thinking } }
|
|
175
175
|
when "message"
|
|
176
|
-
next [] if ToolCall.value(item, :phase).to_s == "commentary"
|
|
177
|
-
|
|
178
176
|
text = response_message_item_text(item)
|
|
179
|
-
|
|
177
|
+
next [] if text.empty?
|
|
178
|
+
|
|
179
|
+
if ToolCall.value(item, :phase).to_s == "commentary"
|
|
180
|
+
[{ type: "thinking", thinking: text }]
|
|
181
|
+
else
|
|
182
|
+
[{ type: "text", text: text }]
|
|
183
|
+
end
|
|
180
184
|
else
|
|
181
185
|
[]
|
|
182
186
|
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Names, aliases, and display paths accepted by the scratchpad command.
|
|
4
|
+
module ScratchpadLanguages
|
|
5
|
+
LANGUAGES = {
|
|
6
|
+
text: { aliases: %w[text txt], display_path: "scratchpad.txt" },
|
|
7
|
+
ruby: { aliases: %w[ruby rb], display_path: "scratchpad.rb", runner: :ruby },
|
|
8
|
+
erb: { aliases: %w[erb], display_path: "scratchpad.erb" },
|
|
9
|
+
crystal: { aliases: %w[crystal cr], display_path: "scratchpad.cr", runner: :crystal },
|
|
10
|
+
elixir: { aliases: %w[elixir ex exs], display_path: "scratchpad.ex", runner: :elixir },
|
|
11
|
+
julia: { aliases: %w[julia jl], display_path: "scratchpad.jl", runner: :julia },
|
|
12
|
+
javascript: { aliases: %w[javascript js jsx mjs cjs], display_path: "scratchpad.js", runner: :node },
|
|
13
|
+
typescript: { aliases: %w[typescript ts tsx], display_path: "scratchpad.ts", runner: :node },
|
|
14
|
+
json: { aliases: %w[json], display_path: "scratchpad.json" },
|
|
15
|
+
markdown: { aliases: %w[markdown md], display_path: "scratchpad.md" },
|
|
16
|
+
yaml: { aliases: %w[yaml yml], display_path: "scratchpad.yml" },
|
|
17
|
+
shell: { aliases: %w[shell sh bash zsh fish], display_path: "scratchpad.sh", runner: :shell },
|
|
18
|
+
makefile: { aliases: %w[makefile make mk], display_path: "scratchpad.mk" },
|
|
19
|
+
html: { aliases: %w[html htm], display_path: "scratchpad.html" },
|
|
20
|
+
css: { aliases: %w[css], display_path: "scratchpad.css" },
|
|
21
|
+
scss: { aliases: %w[scss], display_path: "scratchpad.scss" },
|
|
22
|
+
python: { aliases: %w[python py pyw], display_path: "scratchpad.py", runner: :python },
|
|
23
|
+
go: { aliases: %w[go], display_path: "scratchpad.go", runner: :go },
|
|
24
|
+
rust: { aliases: %w[rust rs], display_path: "scratchpad.rs" },
|
|
25
|
+
java: { aliases: %w[java], display_path: "scratchpad.java" },
|
|
26
|
+
csharp: { aliases: %w[csharp cs c#], display_path: "scratchpad.cs" },
|
|
27
|
+
c: { aliases: %w[c], display_path: "scratchpad.c" },
|
|
28
|
+
cpp: { aliases: %w[cpp cc cxx hpp hh hxx c++], display_path: "scratchpad.cpp" },
|
|
29
|
+
swift: { aliases: %w[swift], display_path: "scratchpad.swift", runner: :swift },
|
|
30
|
+
kotlin: { aliases: %w[kotlin kt kts], display_path: "scratchpad.kt" },
|
|
31
|
+
lua: { aliases: %w[lua], display_path: "scratchpad.lua", runner: :lua },
|
|
32
|
+
sql: { aliases: %w[sql], display_path: "scratchpad.sql" }
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
ALIASES = LANGUAGES.each_with_object({}) do |(language, definition), aliases|
|
|
36
|
+
([language.to_s] + definition.fetch(:aliases)).each do |name|
|
|
37
|
+
aliases[name] = language
|
|
38
|
+
end
|
|
39
|
+
end.freeze
|
|
40
|
+
|
|
41
|
+
module_function
|
|
42
|
+
|
|
43
|
+
def normalize(language)
|
|
44
|
+
value = language.to_s.strip.downcase
|
|
45
|
+
return :text if value.empty?
|
|
46
|
+
|
|
47
|
+
ALIASES.fetch(value) do
|
|
48
|
+
raise ArgumentError, "Unknown scratchpad language #{language.inspect}. Use /scratchpad help for supported languages."
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def display_path(language)
|
|
53
|
+
LANGUAGES.fetch(normalize(language)).fetch(:display_path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def runner(language)
|
|
57
|
+
LANGUAGES.fetch(normalize(language))[:runner]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def runnable?(language)
|
|
61
|
+
!runner(language).nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def help_text
|
|
65
|
+
lines = ["Supported scratchpad languages:"]
|
|
66
|
+
LANGUAGES.each do |language, definition|
|
|
67
|
+
aliases = definition.fetch(:aliases).reject { |name| name == language.to_s }
|
|
68
|
+
suffix = aliases.empty? ? "" : " (#{aliases.join(", ")})"
|
|
69
|
+
lines << " #{language}#{suffix}"
|
|
70
|
+
end
|
|
71
|
+
lines.join("\n")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|