kward 0.83.0 → 0.85.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 +102 -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 +13 -1
- data/doc/api.md +21 -2
- data/doc/composer.md +2 -2
- data/doc/configuration.md +95 -25
- data/doc/editor.md +28 -13
- data/doc/extensibility.md +2 -1
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/git.md +3 -1
- data/doc/pan.md +25 -15
- data/doc/permissions.md +4 -4
- data/doc/platform-support.md +48 -0
- data/doc/plugins.md +464 -15
- data/doc/rpc.md +154 -16
- data/doc/sandboxing.md +11 -5
- data/doc/security.md +10 -3
- data/doc/session-management.md +5 -4
- data/doc/shell.md +62 -45
- data/doc/tabs.md +6 -2
- data/doc/transports.md +15 -0
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +9 -6
- data/doc/workspace-tools.md +9 -0
- data/examples/plugins/space_invaders.rb +1 -1
- data/examples/plugins/stardate_footer.rb +2 -2
- data/examples/plugins/telegram/plugin.rb +1 -1
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +30 -14
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/compaction.rb +9 -3
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +48 -7
- data/lib/kward/cli/plugins.rb +81 -12
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +52 -5
- data/lib/kward/cli/rendering.rb +18 -9
- data/lib/kward/cli/runtime_helpers.rb +228 -71
- data/lib/kward/cli/sessions.rb +9 -5
- data/lib/kward/cli/settings/menus.rb +745 -0
- data/lib/kward/cli/settings/model.rb +327 -0
- data/lib/kward/cli/settings.rb +6 -1055
- data/lib/kward/cli/slash_commands.rb +56 -17
- data/lib/kward/cli/tabs.rb +244 -33
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
- data/lib/kward/cli/worktrees.rb +65 -2
- data/lib/kward/cli.rb +70 -30
- data/lib/kward/compactor.rb +18 -7
- data/lib/kward/config/core.rb +389 -0
- data/lib/kward/config/extensions.rb +96 -0
- data/lib/kward/config/prompts.rb +313 -0
- data/lib/kward/config/settings.rb +250 -0
- data/lib/kward/config_files.rb +14 -994
- data/lib/kward/conversation.rb +31 -2
- data/lib/kward/image_attachments.rb +1 -1
- data/lib/kward/model/client.rb +36 -24
- data/lib/kward/model/copilot_models.rb +2 -2
- data/lib/kward/model/model_info.rb +20 -3
- data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
- data/lib/kward/model/payloads.rb +12 -3
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/model/typesafe_client.rb +78 -0
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +33 -10
- data/lib/kward/permissions/policy.rb +6 -2
- data/lib/kward/plugin_registry.rb +2 -659
- data/lib/kward/plugins/actions.rb +453 -0
- data/lib/kward/plugins/chat_contract.rb +121 -0
- data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
- data/lib/kward/plugins/host.rb +232 -0
- data/lib/kward/plugins/registry.rb +1190 -0
- data/lib/kward/plugins/resources.rb +206 -0
- data/lib/kward/plugins/turn_request.rb +36 -0
- data/lib/kward/plugins/ui.rb +219 -0
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/markdown_code_block.rb +136 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/scratchpad_languages.rb +74 -0
- data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
- data/lib/kward/prompt_interface/editor/state.rb +11 -11
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/layout.rb +1 -1
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +56 -2
- data/lib/kward/prompt_interface/screen.rb +11 -4
- 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 +151 -27
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
- data/lib/kward/pty/detached_run.rb +44 -0
- data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
- data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
- data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
- data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +52 -0
- data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
- data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
- data/lib/kward/rpc/prompt_bridge.rb +25 -0
- data/lib/kward/rpc/server.rb +91 -12
- data/lib/kward/rpc/session_manager.rb +147 -44
- data/lib/kward/rpc/session_tree_rows.rb +2 -2
- data/lib/kward/rpc/tool_metadata.rb +1 -1
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/sandbox/command_runner.rb +1 -1
- data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
- data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
- data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
- data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
- data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
- data/lib/kward/{ekwsh.rb → shell/kwsh.rb} +38 -19
- data/lib/kward/shell/kwshrc.rb +233 -0
- data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
- data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
- data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
- data/lib/kward/skills/trust_store.rb +1 -1
- data/lib/kward/tabs/driver.rb +194 -0
- data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
- data/lib/kward/{ansi.rb → terminal/ansi.rb} +110 -10
- data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
- data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
- data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +12 -0
- data/lib/kward/terminal/text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/base.rb +18 -0
- data/lib/kward/tools/context_for_task.rb +15 -6
- data/lib/kward/tools/edit_file.rb +9 -6
- data/lib/kward/tools/git_commit.rb +13 -7
- data/lib/kward/tools/list_directory.rb +4 -4
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/plugin_tool.rb +41 -0
- data/lib/kward/tools/prepare_shell_command.rb +1 -1
- data/lib/kward/tools/read_file.rb +7 -6
- data/lib/kward/tools/registry.rb +109 -17
- data/lib/kward/tools/run_shell_command.rb +10 -8
- data/lib/kward/tools/search/code.rb +1 -1
- data/lib/kward/tools/summarize_file_structure.rb +5 -5
- data/lib/kward/tools/tool_call.rb +3 -1
- data/lib/kward/tools/typesafe_evaluate.rb +81 -0
- data/lib/kward/tools/workspace_targets.rb +58 -0
- data/lib/kward/tools/write_file.rb +9 -6
- data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
- data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
- data/lib/kward/transport/contracts.rb +200 -0
- data/lib/kward/transport/gateway.rb +79 -35
- data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
- data/lib/kward/transport.rb +1 -200
- data/lib/kward/version.rb +1 -1
- data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
- data/lib/kward/{project_files.rb → workspace/files.rb} +2 -2
- data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
- data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
- 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 +2 -2
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +94 -47
- data/lib/kward/scratchpad_runner.rb +0 -56
- data/lib/kward/tab_driver.rb +0 -90
- /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
- /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
- /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
- /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
- /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
- /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
- /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
- /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
- /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
- /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
|
@@ -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
|
|
|
@@ -3,18 +3,20 @@ require "io/console"
|
|
|
3
3
|
require "pathname"
|
|
4
4
|
require "rbconfig"
|
|
5
5
|
require "thread"
|
|
6
|
-
require_relative "
|
|
6
|
+
require_relative "workspace/files"
|
|
7
7
|
require_relative "image_attachments"
|
|
8
|
-
require_relative "
|
|
8
|
+
require_relative "terminal/image_support"
|
|
9
9
|
require_relative "prompt_history"
|
|
10
10
|
require "tty-cursor"
|
|
11
11
|
require "tty-reader"
|
|
12
12
|
require "tty-screen"
|
|
13
|
-
require_relative "ansi"
|
|
14
|
-
require_relative "
|
|
15
|
-
require_relative "
|
|
16
|
-
require_relative "editor_mode"
|
|
17
|
-
require_relative "diff_view_mode"
|
|
13
|
+
require_relative "terminal/ansi"
|
|
14
|
+
require_relative "terminal/sequences"
|
|
15
|
+
require_relative "terminal/keys"
|
|
16
|
+
require_relative "prompt_interface/editor/editor_mode"
|
|
17
|
+
require_relative "prompt_interface/editor/diff_view_mode"
|
|
18
|
+
require_relative "prompt_interface/editor/scratchpad_languages"
|
|
19
|
+
require_relative "prompt_interface/editor/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"
|
|
@@ -27,11 +29,13 @@ require_relative "prompt_interface/file_overlay"
|
|
|
27
29
|
require_relative "prompt_interface/project_browser"
|
|
28
30
|
require_relative "prompt_interface/selection_prompt"
|
|
29
31
|
require_relative "prompt_interface/question_prompt"
|
|
32
|
+
require_relative "prompt_interface/plugin_ui_requests"
|
|
30
33
|
require_relative "prompt_interface/approval_prompt"
|
|
31
34
|
require_relative "prompt_interface/git_prompt"
|
|
32
35
|
require_relative "prompt_interface/overlay_renderer"
|
|
33
36
|
require_relative "prompt_interface/editor/renderer"
|
|
34
37
|
require_relative "prompt_interface/editor/syntax_highlighter"
|
|
38
|
+
require_relative "prompt_interface/editor/runner"
|
|
35
39
|
require_relative "prompt_interface/editor/auto_close_pairs"
|
|
36
40
|
require_relative "prompt_interface/editor/endwise"
|
|
37
41
|
require_relative "prompt_interface/editor/word_completion"
|
|
@@ -64,12 +68,16 @@ module Kward
|
|
|
64
68
|
# `prompt_interface/` to keep rendering, layout, keyboard handling, and runtime
|
|
65
69
|
# state readable while sharing one terminal state object.
|
|
66
70
|
class PromptInterface
|
|
67
|
-
HELP_TEXT = "Enter sends • Shift+Enter
|
|
71
|
+
HELP_TEXT = "Enter sends • Shift+Enter newline • / commands • @ files • ↑/↓ history • Ctrl+D exits".freeze
|
|
68
72
|
BUSY_HELP_TEXT = "Ctrl+C cancels".freeze
|
|
69
|
-
SPINNER_FRAMES = %w[
|
|
70
|
-
SPINNER_INTERVAL = 0.
|
|
73
|
+
SPINNER_FRAMES = %w[· • ● •].freeze
|
|
74
|
+
SPINNER_INTERVAL = 0.16
|
|
71
75
|
FOOTER_REFRESH_INTERVAL = 1.0
|
|
72
76
|
COMPOSER_STATUS_REFRESH_INTERVAL = 1.0
|
|
77
|
+
COMPLETION_DISPLAY_SECONDS = 0.8
|
|
78
|
+
RESPONSE_ARRIVAL_SECONDS = 0.4
|
|
79
|
+
BUSY_ELAPSED_DELAY_SECONDS = 2.0
|
|
80
|
+
TRANSCRIPT_COMPOSER_GAP_ROWS = 1
|
|
73
81
|
COMPOSER_MAX_INPUT_ROWS = 6
|
|
74
82
|
IMAGE_VIEWER_MAX_ROWS = 8
|
|
75
83
|
TRANSCRIPT_BUFFER_LIMIT = 200_000
|
|
@@ -80,6 +88,7 @@ module Kward
|
|
|
80
88
|
include ProjectBrowser
|
|
81
89
|
include SelectionPrompt
|
|
82
90
|
include QuestionPrompt
|
|
91
|
+
include PluginUIRequests
|
|
83
92
|
include ApprovalPrompt
|
|
84
93
|
include GitPrompt
|
|
85
94
|
include OverlayRenderer
|
|
@@ -89,6 +98,7 @@ module Kward
|
|
|
89
98
|
include EditorEndwise
|
|
90
99
|
include EditorWordCompletion
|
|
91
100
|
include EditorAutoIndent
|
|
101
|
+
include EditorRunner
|
|
92
102
|
include ComposerRenderer
|
|
93
103
|
include ComposerController
|
|
94
104
|
include ModernEditorMode
|
|
@@ -133,24 +143,62 @@ module Kward
|
|
|
133
143
|
end
|
|
134
144
|
end
|
|
135
145
|
|
|
136
|
-
|
|
146
|
+
# Forwards child output while tracking keyboard-protocol resets across chunks.
|
|
147
|
+
class TerminalHandoffOutput
|
|
148
|
+
def initialize(output, on_keyboard_protocol_restore:)
|
|
149
|
+
@output = output
|
|
150
|
+
@on_keyboard_protocol_restore = on_keyboard_protocol_restore
|
|
151
|
+
@pending = +"".b
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def write(value)
|
|
155
|
+
value = value.to_s.b
|
|
156
|
+
detect_keyboard_protocol_restore(value)
|
|
157
|
+
@output.write(value)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def print(*values)
|
|
161
|
+
values.each { |value| write(value) }
|
|
162
|
+
nil
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def flush
|
|
166
|
+
@output.flush if @output.respond_to?(:flush)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private
|
|
170
|
+
|
|
171
|
+
def detect_keyboard_protocol_restore(value)
|
|
172
|
+
@pending << value
|
|
173
|
+
return unless @pending.include?(TerminalSequences::KEYBOARD_PROTOCOL_RESTORE)
|
|
174
|
+
|
|
175
|
+
@on_keyboard_protocol_restore.call
|
|
176
|
+
keep = [TerminalSequences::KEYBOARD_PROTOCOL_RESTORE.bytesize - 1, @pending.bytesize].min
|
|
177
|
+
@pending = keep.positive? ? @pending.byteslice(-keep, keep) : +"".b
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
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)
|
|
137
182
|
@input_io = input
|
|
138
183
|
@output_io = output
|
|
139
184
|
# Logical state may change during a child handoff, but only the owner may render.
|
|
140
185
|
@terminal_owner = :kward
|
|
141
186
|
@terminal_handoff_mode = nil
|
|
187
|
+
@terminal_handoff_keyboard_protocol_reset = false
|
|
142
188
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
143
189
|
@mutex = Mutex.new
|
|
144
190
|
@prompt_history = prompt_history
|
|
145
191
|
@workspace_root = File.expand_path(workspace_root.to_s.empty? ? Dir.pwd : workspace_root)
|
|
146
192
|
@composer = ComposerState.new
|
|
147
193
|
load_history(@prompt_history.values) if @prompt_history
|
|
148
|
-
self.composer_input = @composer.input
|
|
149
|
-
self.composer_cursor = @composer.cursor
|
|
150
194
|
@started = false
|
|
151
195
|
@asking = false
|
|
152
196
|
@busy = false
|
|
153
|
-
@busy_activity = "
|
|
197
|
+
@busy_activity = "thinking"
|
|
198
|
+
@busy_started_at = nil
|
|
199
|
+
@last_busy_elapsed = nil
|
|
200
|
+
@completion_status = nil
|
|
201
|
+
@response_arrival_until = nil
|
|
154
202
|
@queued_count = 0
|
|
155
203
|
@steered_count = 0
|
|
156
204
|
@spinner_frame_index = 0
|
|
@@ -169,6 +217,7 @@ module Kward
|
|
|
169
217
|
@transcript_viewport_rows = 0
|
|
170
218
|
@restoring_transcript = false
|
|
171
219
|
@pending_keys = []
|
|
220
|
+
@handoff_input_buffer = +"".b
|
|
172
221
|
@completion_provider = nil
|
|
173
222
|
@completion_cycle = nil
|
|
174
223
|
@completion_overlay = nil
|
|
@@ -185,12 +234,22 @@ module Kward
|
|
|
185
234
|
@file_open_dismissed_token = nil
|
|
186
235
|
@file_editor_open_status = nil
|
|
187
236
|
@file_mention_paths = nil
|
|
237
|
+
@file_mention_discovery_thread = nil
|
|
238
|
+
@file_mention_discovery_root = nil
|
|
239
|
+
@file_mention_discovery_token = nil
|
|
188
240
|
@project_browser_file_paths = nil
|
|
189
241
|
@project_browser_state = nil
|
|
190
242
|
@image_viewer_state = nil
|
|
191
243
|
@terminal_image_protocol = nil
|
|
192
244
|
@project_browser_restore_after_editor = false
|
|
193
245
|
@editor_state = nil
|
|
246
|
+
@editor_runner_state = nil
|
|
247
|
+
@editor_runner_completion = nil
|
|
248
|
+
@editor_runner_output_visible = false
|
|
249
|
+
@editor_runner_selection_anchor = nil
|
|
250
|
+
@editor_runner_selection_cursor = nil
|
|
251
|
+
@editor_runner_mouse_dragging = false
|
|
252
|
+
@editor_runner_last_click = nil
|
|
194
253
|
@editor_agent_suspended = false
|
|
195
254
|
@editor_prompt_active = false
|
|
196
255
|
@editor_prompt_input = ""
|
|
@@ -232,6 +291,7 @@ module Kward
|
|
|
232
291
|
@editor_line_numbers_source = editor_line_numbers_source
|
|
233
292
|
@diff_view = DiffViewMode.normalize(diff_view)
|
|
234
293
|
@diff_view_source = diff_view_source
|
|
294
|
+
@editor_runners_source = editor_runners_source
|
|
235
295
|
end
|
|
236
296
|
|
|
237
297
|
def update_slash_commands(commands)
|
|
@@ -439,6 +499,12 @@ module Kward
|
|
|
439
499
|
return false unless opened
|
|
440
500
|
|
|
441
501
|
run_editor
|
|
502
|
+
rescue StandardError
|
|
503
|
+
@mutex.synchronize do
|
|
504
|
+
close_editor if editor_active?
|
|
505
|
+
render_prompt_locked if @started
|
|
506
|
+
end
|
|
507
|
+
raise
|
|
442
508
|
end
|
|
443
509
|
|
|
444
510
|
def scratchpad(language = :text)
|
|
@@ -524,7 +590,8 @@ module Kward
|
|
|
524
590
|
if key.nil?
|
|
525
591
|
resized = handle_resize_locked
|
|
526
592
|
footer_refreshed = tick_footer_locked
|
|
527
|
-
|
|
593
|
+
completion_changed = tick_completion_locked
|
|
594
|
+
render_prompt_locked if resized || footer_refreshed || completion_changed
|
|
528
595
|
else
|
|
529
596
|
result = handle_key(key)
|
|
530
597
|
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
|
|
@@ -667,11 +734,11 @@ module Kward
|
|
|
667
734
|
end
|
|
668
735
|
end
|
|
669
736
|
|
|
670
|
-
def with_inline_terminal_handoff(&block)
|
|
671
|
-
with_terminal_handoff(inline: true, &block)
|
|
737
|
+
def with_inline_terminal_handoff(preserve_tab_keybindings: false, &block)
|
|
738
|
+
with_terminal_handoff(inline: true, preserve_tab_keybindings: preserve_tab_keybindings, &block)
|
|
672
739
|
end
|
|
673
740
|
|
|
674
|
-
def with_terminal_handoff(inline: false)
|
|
741
|
+
def with_terminal_handoff(inline: false, preserve_tab_keybindings: false)
|
|
675
742
|
start
|
|
676
743
|
input = nil
|
|
677
744
|
output = nil
|
|
@@ -687,13 +754,21 @@ module Kward
|
|
|
687
754
|
end
|
|
688
755
|
disable_editor_mouse_reporting(force: true)
|
|
689
756
|
print_output_locked(BRACKETED_PASTE_RESTORE)
|
|
690
|
-
print_output_locked(KEYBOARD_PROTOCOL_RESTORE)
|
|
757
|
+
print_output_locked(KEYBOARD_PROTOCOL_RESTORE) unless preserve_tab_keybindings
|
|
758
|
+
@handoff_input_buffer.clear
|
|
759
|
+
@terminal_handoff_keyboard_protocol_reset = false
|
|
691
760
|
restore_editor_cursor_shape_locked
|
|
692
761
|
set_cursor_visible_locked(true, force: true)
|
|
693
762
|
flush_output_locked
|
|
694
763
|
restore_console_mode_locked
|
|
695
764
|
input = @input_io
|
|
696
|
-
output =
|
|
765
|
+
output = if preserve_tab_keybindings
|
|
766
|
+
TerminalHandoffOutput.new(@output_io, on_keyboard_protocol_restore: -> {
|
|
767
|
+
@terminal_handoff_keyboard_protocol_reset = true
|
|
768
|
+
})
|
|
769
|
+
else
|
|
770
|
+
@output_io
|
|
771
|
+
end
|
|
697
772
|
transition = method(:switch_terminal_handoff_to_exclusive)
|
|
698
773
|
@terminal_handoff_mode = inline ? :inline : :exclusive
|
|
699
774
|
@terminal_owner = :child
|
|
@@ -707,8 +782,12 @@ module Kward
|
|
|
707
782
|
restore_scroll_region_locked
|
|
708
783
|
print_output_locked(TerminalSequences::SGR_RESET)
|
|
709
784
|
enter_raw_mode_locked
|
|
710
|
-
|
|
785
|
+
if !preserve_tab_keybindings || @terminal_handoff_keyboard_protocol_reset
|
|
786
|
+
print_output_locked(KEYBOARD_PROTOCOL_ENABLE)
|
|
787
|
+
end
|
|
711
788
|
print_output_locked(BRACKETED_PASTE_ENABLE)
|
|
789
|
+
@handoff_input_buffer.clear
|
|
790
|
+
@terminal_handoff_keyboard_protocol_reset = false
|
|
712
791
|
disable_editor_mouse_reporting(force: true)
|
|
713
792
|
restore_editor_cursor_shape_locked(force: true)
|
|
714
793
|
@cursor_visible = nil
|
|
@@ -802,8 +881,6 @@ module Kward
|
|
|
802
881
|
@completion_cycle = nil
|
|
803
882
|
@completion_overlay = nil
|
|
804
883
|
@prompt_label = snapshot[:prompt_label].to_s.empty? ? "You>" : snapshot[:prompt_label].to_s
|
|
805
|
-
self.composer_input = @composer.input
|
|
806
|
-
self.composer_cursor = @composer.cursor
|
|
807
884
|
@last_composer_rows = []
|
|
808
885
|
end
|
|
809
886
|
|
|
@@ -815,6 +892,11 @@ module Kward
|
|
|
815
892
|
|
|
816
893
|
def restore_editor_snapshot_locked(snapshot)
|
|
817
894
|
editor_was_active = editor_active?
|
|
895
|
+
cancel_editor_runner
|
|
896
|
+
@editor_runner_state = nil
|
|
897
|
+
@editor_runner_completion = nil
|
|
898
|
+
@editor_runner_output_visible = false
|
|
899
|
+
clear_editor_runner_selection
|
|
818
900
|
@editor_state = snapshot[:editor_state]&.dup
|
|
819
901
|
editor_is_active = editor_active?
|
|
820
902
|
|
|
@@ -839,17 +921,21 @@ module Kward
|
|
|
839
921
|
end
|
|
840
922
|
end
|
|
841
923
|
|
|
842
|
-
def begin_busy_input(message = "You>", activity: "
|
|
924
|
+
def begin_busy_input(message = "You>", activity: "thinking")
|
|
843
925
|
start
|
|
844
926
|
@mutex.synchronize do
|
|
845
927
|
@prompt_label = message.to_s
|
|
846
928
|
@busy_activity = normalize_busy_activity(activity)
|
|
929
|
+
@completion_status = nil
|
|
930
|
+
@response_arrival_until = nil
|
|
847
931
|
self.composer_input = ""
|
|
848
932
|
self.composer_cursor = 0
|
|
849
933
|
@composer.clear_attachments
|
|
850
934
|
@pending_keys.clear
|
|
851
935
|
@asking = true
|
|
852
936
|
@busy = true
|
|
937
|
+
@busy_started_at = monotonic_now
|
|
938
|
+
@last_busy_elapsed = nil
|
|
853
939
|
@queued_count = 0
|
|
854
940
|
@steered_count = 0
|
|
855
941
|
reset_spinner_locked
|
|
@@ -858,6 +944,17 @@ module Kward
|
|
|
858
944
|
end
|
|
859
945
|
end
|
|
860
946
|
|
|
947
|
+
def update_busy_activity(activity)
|
|
948
|
+
@mutex.synchronize do
|
|
949
|
+
value = normalize_busy_activity(activity)
|
|
950
|
+
return false if value == @busy_activity
|
|
951
|
+
|
|
952
|
+
@busy_activity = value
|
|
953
|
+
render_prompt_locked if @started && @asking && @busy
|
|
954
|
+
true
|
|
955
|
+
end
|
|
956
|
+
end
|
|
957
|
+
|
|
861
958
|
def set_queued_count(count)
|
|
862
959
|
@mutex.synchronize do
|
|
863
960
|
@queued_count = count.to_i
|
|
@@ -877,15 +974,18 @@ module Kward
|
|
|
877
974
|
def clear_steered_count
|
|
878
975
|
@mutex.synchronize do
|
|
879
976
|
@steered_count = 0
|
|
880
|
-
@busy_activity = "
|
|
977
|
+
@busy_activity = "thinking"
|
|
881
978
|
render_prompt_locked if @asking
|
|
882
979
|
end
|
|
883
980
|
end
|
|
884
981
|
|
|
885
982
|
def finish_busy_input
|
|
886
983
|
@mutex.synchronize do
|
|
984
|
+
@last_busy_elapsed = monotonic_now - @busy_started_at if @busy_started_at
|
|
985
|
+
@busy_started_at = nil
|
|
887
986
|
@busy = false
|
|
888
|
-
@busy_activity = "
|
|
987
|
+
@busy_activity = "thinking"
|
|
988
|
+
@response_arrival_until = nil
|
|
889
989
|
@queued_count = 0
|
|
890
990
|
@steered_count = 0
|
|
891
991
|
@asking = true
|
|
@@ -893,6 +993,24 @@ module Kward
|
|
|
893
993
|
end
|
|
894
994
|
end
|
|
895
995
|
|
|
996
|
+
def show_response_arrival
|
|
997
|
+
@mutex.synchronize do
|
|
998
|
+
@response_arrival_until = monotonic_now + RESPONSE_ARRIVAL_SECONDS
|
|
999
|
+
render_prompt_locked if @started && @asking && @busy
|
|
1000
|
+
end
|
|
1001
|
+
end
|
|
1002
|
+
|
|
1003
|
+
def show_completion(status)
|
|
1004
|
+
@mutex.synchronize do
|
|
1005
|
+
@completion_status = completion_status(status)
|
|
1006
|
+
render_prompt_locked if @started && @asking
|
|
1007
|
+
end
|
|
1008
|
+
end
|
|
1009
|
+
|
|
1010
|
+
def filter_terminal_handoff_input(chunk)
|
|
1011
|
+
@mutex.synchronize { filter_terminal_handoff_input_locked(chunk) }
|
|
1012
|
+
end
|
|
1013
|
+
|
|
896
1014
|
def poll_input
|
|
897
1015
|
key = read_key(nonblock: true)
|
|
898
1016
|
@mutex.synchronize do
|
|
@@ -916,7 +1034,8 @@ module Kward
|
|
|
916
1034
|
resized = handle_resize_locked
|
|
917
1035
|
spun = tick_spinner_locked
|
|
918
1036
|
footer_refreshed = tick_footer_locked
|
|
919
|
-
|
|
1037
|
+
completion_changed = tick_completion_locked
|
|
1038
|
+
render_prompt_locked if resized || spun || footer_refreshed || completion_changed
|
|
920
1039
|
return nil
|
|
921
1040
|
end
|
|
922
1041
|
|
|
@@ -1106,6 +1225,11 @@ module Kward
|
|
|
1106
1225
|
|
|
1107
1226
|
def reset_workspace_file_state_locked
|
|
1108
1227
|
@file_mention_paths = nil
|
|
1228
|
+
@file_mention_discovery_thread = nil
|
|
1229
|
+
@file_mention_discovery_root = nil
|
|
1230
|
+
@file_mention_discovery_token = nil
|
|
1231
|
+
@file_overlay_match_cache = nil
|
|
1232
|
+
@project_browser_search_cache = nil
|
|
1109
1233
|
@project_browser_file_paths = nil
|
|
1110
1234
|
@file_mention_path_entries_paths = nil
|
|
1111
1235
|
@file_mention_path_entries = nil
|
|
@@ -11,6 +11,7 @@ module Kward
|
|
|
11
11
|
{ name: "session", description: "Open saved sessions or name the current session.", argument_hint: "[path]|name [name]" },
|
|
12
12
|
{ name: "resume", description: "Alias for /session.", argument_hint: "[path]" },
|
|
13
13
|
{ name: "rename", description: "Rename the current session.", argument_hint: "<name>" },
|
|
14
|
+
{ name: "name", description: "Rename the current session and tab.", argument_hint: "<name>" },
|
|
14
15
|
{ name: "clone", description: "Clone the current session.", argument_hint: "" },
|
|
15
16
|
{ name: "fork", description: "Fork from an earlier prompt into a new session.", argument_hint: "" },
|
|
16
17
|
{ name: "rewind", description: "Revisit an earlier prompt and try a different direction.", argument_hint: "" },
|
|
@@ -29,10 +30,10 @@ module Kward
|
|
|
29
30
|
{ name: "files", description: "Browse project files.", argument_hint: "" },
|
|
30
31
|
{ name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
|
|
31
32
|
{ name: "capture", description: "Run a command with bounded transcript capture.", argument_hint: "<command>" },
|
|
32
|
-
{ name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[
|
|
33
|
+
{ name: "scratchpad", description: "Open an unsaved editor buffer.", argument_hint: "[language|help]" },
|
|
33
34
|
{ name: "pty", description: "Run a command in an interactive PTY passthrough session.", argument_hint: "<command>" },
|
|
34
35
|
{ 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|
|
|
36
|
+
{ name: "worktree", description: "Manage the active tab's linked Git worktree.", argument_hint: "[activate|detach|status|merge [resolve|continue|abort]|remove]" },
|
|
36
37
|
{ name: "status", description: "Show the current status message.", argument_hint: "" },
|
|
37
38
|
{ name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
|
|
38
39
|
{ 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
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require "thread"
|
|
2
|
+
|
|
3
|
+
# Namespace for the Kward CLI agent runtime.
|
|
4
|
+
module Kward
|
|
5
|
+
# Owns a command that continues after its terminal has been returned to Kward.
|
|
6
|
+
class DetachedRun
|
|
7
|
+
attr_reader :sink
|
|
8
|
+
|
|
9
|
+
def initialize(sink:, canceler: nil, &work)
|
|
10
|
+
@sink = sink
|
|
11
|
+
@canceler = canceler
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
@result = nil
|
|
14
|
+
@thread = Thread.new do
|
|
15
|
+
begin
|
|
16
|
+
result = work.call
|
|
17
|
+
@mutex.synchronize { @result = result }
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
@mutex.synchronize { @result = e }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
@thread.report_on_exception = false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def complete?
|
|
26
|
+
!@thread.alive?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def result
|
|
30
|
+
return unless complete?
|
|
31
|
+
|
|
32
|
+
@thread.join
|
|
33
|
+
@mutex.synchronize { @result }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def join(timeout = nil)
|
|
37
|
+
@thread.join(timeout)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def cancel!
|
|
41
|
+
@canceler&.call
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|