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.
Files changed (193) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +102 -15
  3. data/CONTRIBUTING.md +74 -0
  4. data/Gemfile.lock +8 -2
  5. data/README.md +21 -1
  6. data/Rakefile +46 -2
  7. data/SECURITY.md +31 -0
  8. data/doc/agent-tools.md +13 -1
  9. data/doc/api.md +21 -2
  10. data/doc/composer.md +2 -2
  11. data/doc/configuration.md +95 -25
  12. data/doc/editor.md +28 -13
  13. data/doc/extensibility.md +2 -1
  14. data/doc/files.md +8 -4
  15. data/doc/getting-started.md +3 -0
  16. data/doc/git.md +3 -1
  17. data/doc/pan.md +25 -15
  18. data/doc/permissions.md +4 -4
  19. data/doc/platform-support.md +48 -0
  20. data/doc/plugins.md +464 -15
  21. data/doc/rpc.md +154 -16
  22. data/doc/sandboxing.md +11 -5
  23. data/doc/security.md +10 -3
  24. data/doc/session-management.md +5 -4
  25. data/doc/shell.md +62 -45
  26. data/doc/tabs.md +6 -2
  27. data/doc/transports.md +15 -0
  28. data/doc/troubleshooting.md +12 -2
  29. data/doc/usage.md +9 -6
  30. data/doc/workspace-tools.md +9 -0
  31. data/examples/plugins/space_invaders.rb +1 -1
  32. data/examples/plugins/stardate_footer.rb +2 -2
  33. data/examples/plugins/telegram/plugin.rb +1 -1
  34. data/kward.gemspec +5 -4
  35. data/lib/kward/agent.rb +30 -14
  36. data/lib/kward/cli/auth_commands.rb +34 -13
  37. data/lib/kward/cli/commands.rb +83 -62
  38. data/lib/kward/cli/compaction.rb +9 -3
  39. data/lib/kward/cli/doctor.rb +39 -17
  40. data/lib/kward/cli/hook_commands.rb +22 -12
  41. data/lib/kward/cli/interactive_turn.rb +48 -7
  42. data/lib/kward/cli/plugins.rb +81 -12
  43. data/lib/kward/cli/project_skills_commands.rb +8 -4
  44. data/lib/kward/cli/prompt_interface.rb +52 -5
  45. data/lib/kward/cli/rendering.rb +18 -9
  46. data/lib/kward/cli/runtime_helpers.rb +228 -71
  47. data/lib/kward/cli/sessions.rb +9 -5
  48. data/lib/kward/cli/settings/menus.rb +745 -0
  49. data/lib/kward/cli/settings/model.rb +327 -0
  50. data/lib/kward/cli/settings.rb +6 -1055
  51. data/lib/kward/cli/slash_commands.rb +56 -17
  52. data/lib/kward/cli/tabs.rb +244 -33
  53. data/lib/kward/cli/tool_summaries.rb +14 -0
  54. data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +14 -7
  55. data/lib/kward/cli/worktrees.rb +65 -2
  56. data/lib/kward/cli.rb +70 -30
  57. data/lib/kward/compactor.rb +18 -7
  58. data/lib/kward/config/core.rb +389 -0
  59. data/lib/kward/config/extensions.rb +96 -0
  60. data/lib/kward/config/prompts.rb +313 -0
  61. data/lib/kward/config/settings.rb +250 -0
  62. data/lib/kward/config_files.rb +14 -994
  63. data/lib/kward/conversation.rb +31 -2
  64. data/lib/kward/image_attachments.rb +1 -1
  65. data/lib/kward/model/client.rb +36 -24
  66. data/lib/kward/model/copilot_models.rb +2 -2
  67. data/lib/kward/model/model_info.rb +20 -3
  68. data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
  69. data/lib/kward/model/payloads.rb +12 -3
  70. data/lib/kward/model/provider_catalog.rb +5 -0
  71. data/lib/kward/model/stream_parser.rb +20 -4
  72. data/lib/kward/model/typesafe_client.rb +78 -0
  73. data/lib/kward/pan/index.html.erb +3 -3
  74. data/lib/kward/pan/server.rb +33 -10
  75. data/lib/kward/permissions/policy.rb +6 -2
  76. data/lib/kward/plugin_registry.rb +2 -659
  77. data/lib/kward/plugins/actions.rb +453 -0
  78. data/lib/kward/plugins/chat_contract.rb +121 -0
  79. data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
  80. data/lib/kward/plugins/host.rb +232 -0
  81. data/lib/kward/plugins/registry.rb +1190 -0
  82. data/lib/kward/plugins/resources.rb +206 -0
  83. data/lib/kward/plugins/turn_request.rb +36 -0
  84. data/lib/kward/plugins/ui.rb +219 -0
  85. data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
  86. data/lib/kward/prompt_interface/composer_state.rb +33 -24
  87. data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
  88. data/lib/kward/prompt_interface/editor/controller.rb +30 -33
  89. data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
  90. data/lib/kward/prompt_interface/editor/markdown_code_block.rb +136 -0
  91. data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
  92. data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
  93. data/lib/kward/prompt_interface/editor/runner.rb +362 -0
  94. data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
  95. data/lib/kward/prompt_interface/editor/scratchpad_languages.rb +74 -0
  96. data/lib/kward/prompt_interface/editor/scratchpad_runner.rb +182 -0
  97. data/lib/kward/prompt_interface/editor/state.rb +11 -11
  98. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
  99. data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
  100. data/lib/kward/prompt_interface/file_overlay.rb +71 -15
  101. data/lib/kward/prompt_interface/key_handler.rb +67 -0
  102. data/lib/kward/prompt_interface/layout.rb +1 -1
  103. data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
  104. data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
  105. data/lib/kward/prompt_interface/project_browser.rb +415 -14
  106. data/lib/kward/prompt_interface/runtime_state.rb +56 -2
  107. data/lib/kward/prompt_interface/screen.rb +11 -4
  108. data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
  109. data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
  110. data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
  111. data/lib/kward/prompt_interface.rb +151 -27
  112. data/lib/kward/prompts/commands.rb +3 -2
  113. data/lib/kward/prompts.rb +1 -1
  114. data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
  115. data/lib/kward/pty/detached_run.rb +44 -0
  116. data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +104 -30
  117. data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
  118. data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
  119. data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +52 -0
  120. data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
  121. data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
  122. data/lib/kward/rpc/prompt_bridge.rb +25 -0
  123. data/lib/kward/rpc/server.rb +91 -12
  124. data/lib/kward/rpc/session_manager.rb +147 -44
  125. data/lib/kward/rpc/session_tree_rows.rb +2 -2
  126. data/lib/kward/rpc/tool_metadata.rb +1 -1
  127. data/lib/kward/rpc/transcript_normalizer.rb +7 -3
  128. data/lib/kward/sandbox/command_runner.rb +1 -1
  129. data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
  130. data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
  131. data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
  132. data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
  133. data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
  134. data/lib/kward/{ekwsh.rb → shell/kwsh.rb} +38 -19
  135. data/lib/kward/shell/kwshrc.rb +233 -0
  136. data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +121 -28
  137. data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +2 -0
  138. data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
  139. data/lib/kward/skills/trust_store.rb +1 -1
  140. data/lib/kward/tabs/driver.rb +194 -0
  141. data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
  142. data/lib/kward/{ansi.rb → terminal/ansi.rb} +110 -10
  143. data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
  144. data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
  145. data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +12 -0
  146. data/lib/kward/terminal/text.rb +121 -0
  147. data/lib/kward/text_matcher.rb +18 -0
  148. data/lib/kward/tools/base.rb +18 -0
  149. data/lib/kward/tools/context_for_task.rb +15 -6
  150. data/lib/kward/tools/edit_file.rb +9 -6
  151. data/lib/kward/tools/git_commit.rb +13 -7
  152. data/lib/kward/tools/list_directory.rb +4 -4
  153. data/lib/kward/tools/open_editor.rb +41 -0
  154. data/lib/kward/tools/plugin_tool.rb +41 -0
  155. data/lib/kward/tools/prepare_shell_command.rb +1 -1
  156. data/lib/kward/tools/read_file.rb +7 -6
  157. data/lib/kward/tools/registry.rb +109 -17
  158. data/lib/kward/tools/run_shell_command.rb +10 -8
  159. data/lib/kward/tools/search/code.rb +1 -1
  160. data/lib/kward/tools/summarize_file_structure.rb +5 -5
  161. data/lib/kward/tools/tool_call.rb +3 -1
  162. data/lib/kward/tools/typesafe_evaluate.rb +81 -0
  163. data/lib/kward/tools/workspace_targets.rb +58 -0
  164. data/lib/kward/tools/write_file.rb +9 -6
  165. data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
  166. data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
  167. data/lib/kward/transport/contracts.rb +200 -0
  168. data/lib/kward/transport/gateway.rb +79 -35
  169. data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
  170. data/lib/kward/transport.rb +1 -200
  171. data/lib/kward/version.rb +1 -1
  172. data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
  173. data/lib/kward/{project_files.rb → workspace/files.rb} +2 -2
  174. data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
  175. data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
  176. data/templates/default/kward_navigation.rb +1 -0
  177. data/templates/default/layout/html/footer.erb +10 -0
  178. data/templates/default/layout/html/headers.erb +23 -0
  179. data/templates/default/layout/html/layout.erb +2 -2
  180. data/templates/default/layout/html/setup.rb +41 -2
  181. metadata +94 -47
  182. data/lib/kward/scratchpad_runner.rb +0 -56
  183. data/lib/kward/tab_driver.rb +0 -90
  184. /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
  185. /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
  186. /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
  187. /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
  188. /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
  189. /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
  190. /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
  191. /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
  192. /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
  193. /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.map do |command|
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: slash_command_value(command, :name).to_s,
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.reject { |command| command[:name].empty? }.sort_by { |command| command[:name] }
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
- @slash_commands.select { |command| command[:name].downcase.start_with?(prefix) }.first(8)
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("#{transcript_label(label)}>", *label_styles(label))} ")
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
- [:gray, :bold]
128
+ [:metadata, :bold]
124
129
  when "Assistant", "Kward"
125
- [:green, :bold]
130
+ [:activity, :bold]
126
131
  when "Tool", "Tool output"
127
- [:cyan, :bold]
132
+ [:tool, :bold]
128
133
  when "Tool failed"
129
- [:red, :bold]
134
+ [:failure, :bold]
130
135
  when "Retry"
131
- [:yellow, :bold]
136
+ [:caution, :bold]
132
137
  else
133
- [:gray, :bold]
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 "project_files"
6
+ require_relative "workspace/files"
7
7
  require_relative "image_attachments"
8
- require_relative "terminal_image_support"
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 "terminal_sequences"
15
- require_relative "terminal_keys"
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 inserts newline • ↑/↓ history • Ctrl+D exits empty prompt".freeze
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[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
70
- SPINNER_INTERVAL = 0.1
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
- 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)
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 = "streaming"
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
- render_prompt_locked if resized || footer_refreshed
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 = @output_io
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
- print_output_locked(KEYBOARD_PROTOCOL_ENABLE)
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: "streaming")
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 = "streaming"
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 = "streaming"
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
- render_prompt_locked if resized || spun || footer_refreshed
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: "[text|markdown|ruby]" },
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|merge abort|remove]" },
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
 
@@ -1,4 +1,4 @@
1
- require_relative "terminal_sequences"
1
+ require_relative "../terminal/sequences"
2
2
 
3
3
  # Namespace for the Kward CLI agent runtime.
4
4
  module Kward
@@ -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