kward 0.70.0 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +1 -1
- data/CHANGELOG.md +89 -3
- data/Gemfile +2 -0
- data/Gemfile.lock +90 -2
- data/README.md +34 -6
- data/Rakefile +96 -0
- data/doc/agent-tools.md +52 -0
- data/doc/api.md +92 -0
- data/doc/authentication.md +58 -23
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +102 -13
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +83 -0
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +74 -3
- data/doc/releasing.md +45 -8
- data/doc/rpc.md +77 -15
- data/doc/session-management.md +254 -0
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +60 -15
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +144 -0
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +41 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/openrouter_commands.rb +55 -0
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +111 -6
- data/lib/kward/cli/rendering.rb +11 -6
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +262 -13
- data/lib/kward/cli/settings.rb +216 -37
- data/lib/kward/cli/slash_commands.rb +439 -8
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +171 -26
- data/lib/kward/compactor.rb +4 -1
- data/lib/kward/config_files.rb +125 -5
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +59 -22
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/model/client.rb +37 -50
- data/lib/kward/model/context_usage.rb +13 -6
- data/lib/kward/model/model_info.rb +92 -16
- data/lib/kward/model/payloads.rb +2 -0
- data/lib/kward/openrouter_model_cache.rb +120 -0
- data/lib/kward/plugin_registry.rb +108 -1
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/banner.rb +16 -51
- data/lib/kward/prompt_interface/composer_controller.rb +124 -83
- data/lib/kward/prompt_interface/composer_renderer.rb +116 -14
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +416 -43
- data/lib/kward/prompt_interface/layout.rb +2 -2
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +32 -13
- data/lib/kward/prompt_interface/question_prompt.rb +122 -82
- data/lib/kward/prompt_interface/runtime_state.rb +49 -1
- data/lib/kward/prompt_interface/screen.rb +17 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +511 -58
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +13 -16
- data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
- data/lib/kward/prompt_interface.rb +307 -35
- data/lib/kward/prompts/commands.rb +7 -1
- data/lib/kward/prompts.rb +4 -2
- data/lib/kward/rpc/server.rb +45 -11
- data/lib/kward/rpc/session_manager.rb +52 -53
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +67 -4
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/telemetry/logger.rb +5 -3
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tool_output_compactor.rb +127 -0
- data/lib/kward/tools/base.rb +8 -2
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +92 -15
- data/lib/kward/tools/retrieve_tool_output.rb +71 -0
- data/lib/kward/tools/search/web.rb +2 -2
- data/lib/kward/tools/summarize_file_structure.rb +29 -0
- data/lib/kward/tools/tool_call.rb +12 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +154 -12
- data/templates/default/fulldoc/html/css/kward.css +362 -42
- data/templates/default/fulldoc/html/full_list.erb +107 -0
- data/templates/default/fulldoc/html/js/kward.js +161 -2
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/kward_navigation.rb +102 -0
- data/templates/default/layout/html/layout.erb +43 -10
- data/templates/default/layout/html/setup.rb +39 -38
- metadata +65 -3
- data/lib/kward/resources/avatar_kward_logo.rb +0 -50
- data/lib/kward/resources/pixel_logo.rb +0 -232
data/lib/kward/cli.rb
CHANGED
|
@@ -13,6 +13,7 @@ require_relative "cli_transcript_formatter"
|
|
|
13
13
|
require_relative "model/context_usage"
|
|
14
14
|
require_relative "events"
|
|
15
15
|
require_relative "export_path"
|
|
16
|
+
require_relative "ekwsh"
|
|
16
17
|
require_relative "auth/anthropic_oauth"
|
|
17
18
|
require_relative "auth/github_oauth"
|
|
18
19
|
require_relative "auth/openrouter_api_key"
|
|
@@ -29,9 +30,12 @@ require_relative "model/retry_message"
|
|
|
29
30
|
require_relative "rpc/server"
|
|
30
31
|
require_relative "session_diff"
|
|
31
32
|
require_relative "session_store"
|
|
33
|
+
require_relative "tab_store"
|
|
34
|
+
require_relative "session_trash"
|
|
32
35
|
require_relative "session_tree_renderer"
|
|
33
36
|
require_relative "starter_pack_installer"
|
|
34
37
|
require_relative "steering"
|
|
38
|
+
require_relative "workers"
|
|
35
39
|
require_relative "tools/tool_call"
|
|
36
40
|
require_relative "tools/registry"
|
|
37
41
|
require_relative "telemetry/stats"
|
|
@@ -41,15 +45,18 @@ require_relative "cli/auth_commands"
|
|
|
41
45
|
require_relative "cli/doctor"
|
|
42
46
|
require_relative "cli/sysprompt"
|
|
43
47
|
require_relative "cli/stats"
|
|
48
|
+
require_relative "cli/openrouter_commands"
|
|
44
49
|
require_relative "cli/runtime_helpers"
|
|
45
50
|
require_relative "cli/slash_commands"
|
|
46
51
|
require_relative "cli/memory_commands"
|
|
47
52
|
require_relative "cli/settings"
|
|
48
53
|
require_relative "cli/sessions"
|
|
54
|
+
require_relative "cli/tabs"
|
|
49
55
|
require_relative "cli/compaction"
|
|
50
56
|
require_relative "cli/rendering"
|
|
51
57
|
require_relative "cli/prompt_interface"
|
|
52
58
|
require_relative "cli/plugins"
|
|
59
|
+
require_relative "cli/git"
|
|
53
60
|
require_relative "cli/interactive_turn"
|
|
54
61
|
require_relative "cli/tool_summaries"
|
|
55
62
|
|
|
@@ -58,8 +65,6 @@ module Kward
|
|
|
58
65
|
# Command-line interface for interactive chat, one-shot prompts, login,
|
|
59
66
|
# telemetry export, Pan server mode, and the experimental JSON-RPC backend.
|
|
60
67
|
class CLI
|
|
61
|
-
# Order from the captain: The next line shall never be deleted.
|
|
62
|
-
STATUS_MESSAGE = "This is a totally important status message about a non-existing status. Hi ChatGPT 👋"
|
|
63
68
|
RESTORED_TOOL_OUTPUT_LIMIT = 2_000
|
|
64
69
|
INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT = 10
|
|
65
70
|
STREAM_RENDER_INTERVAL = 0.025
|
|
@@ -72,15 +77,18 @@ module Kward
|
|
|
72
77
|
include CLI::Doctor
|
|
73
78
|
include CLI::Sysprompt
|
|
74
79
|
include CLI::Stats
|
|
80
|
+
include CLI::OpenRouterCommands
|
|
75
81
|
include CLI::RuntimeHelpers
|
|
76
82
|
include CLI::SlashCommands
|
|
77
83
|
include CLI::MemoryCommands
|
|
78
84
|
include CLI::Settings
|
|
79
85
|
include CLI::Sessions
|
|
86
|
+
include CLI::Tabs
|
|
80
87
|
include CLI::CompactionCommands
|
|
81
88
|
include CLI::Rendering
|
|
82
89
|
include CLI::PromptInterfaceSupport
|
|
83
90
|
include CLI::Plugins
|
|
91
|
+
include CLI::GitCommands
|
|
84
92
|
include CLI::InteractiveTurn
|
|
85
93
|
include CLI::ToolSummaries
|
|
86
94
|
|
|
@@ -97,6 +105,11 @@ module Kward
|
|
|
97
105
|
@plugin_registry = nil
|
|
98
106
|
@working_directory = nil
|
|
99
107
|
@prompt_delimited = false
|
|
108
|
+
@requested_mode = "auto"
|
|
109
|
+
@experimental_workers = false
|
|
110
|
+
@foreground_turn_active = false
|
|
111
|
+
@pending_reasoning_config = nil
|
|
112
|
+
@pending_reasoning_config_mutex = Mutex.new
|
|
100
113
|
@color_enabled = ANSI.enabled?($stdout)
|
|
101
114
|
end
|
|
102
115
|
|
|
@@ -161,6 +174,28 @@ module Kward
|
|
|
161
174
|
return
|
|
162
175
|
end
|
|
163
176
|
|
|
177
|
+
if @argv.first == "edit"
|
|
178
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
179
|
+
print_command_help("edit")
|
|
180
|
+
return
|
|
181
|
+
end
|
|
182
|
+
raise ArgumentError, command_usage("edit") unless @argv.length == 2
|
|
183
|
+
|
|
184
|
+
edit_file_command(@argv[1])
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
if @argv.first == "count-tests"
|
|
189
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
190
|
+
print_command_help("count-tests")
|
|
191
|
+
return
|
|
192
|
+
end
|
|
193
|
+
raise ArgumentError, command_usage("count-tests") unless @argv.length == 1
|
|
194
|
+
|
|
195
|
+
print_test_count
|
|
196
|
+
return
|
|
197
|
+
end
|
|
198
|
+
|
|
164
199
|
if @argv.first == "sysprompt"
|
|
165
200
|
if help_option_arguments?(@argv[1..] || [])
|
|
166
201
|
print_command_help("sysprompt")
|
|
@@ -178,7 +213,7 @@ module Kward
|
|
|
178
213
|
end
|
|
179
214
|
raise ArgumentError, command_usage("rpc") unless @argv.length == 1
|
|
180
215
|
|
|
181
|
-
Kward::RPC::Server.new(input: @stdin, output: $stdout, client: @client).run
|
|
216
|
+
Kward::RPC::Server.new(input: @stdin, output: $stdout, client: @client, experimental_workers: @experimental_workers).run
|
|
182
217
|
return
|
|
183
218
|
end
|
|
184
219
|
|
|
@@ -193,6 +228,16 @@ module Kward
|
|
|
193
228
|
return
|
|
194
229
|
end
|
|
195
230
|
|
|
231
|
+
if @argv.first == "openrouter"
|
|
232
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
233
|
+
print_command_help("openrouter")
|
|
234
|
+
return
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
handle_openrouter_command(@argv[1..] || [])
|
|
238
|
+
return
|
|
239
|
+
end
|
|
240
|
+
|
|
196
241
|
if pan_mode?
|
|
197
242
|
if help_option_arguments?(@argv[1..] || [])
|
|
198
243
|
print_command_help("pan")
|
|
@@ -218,53 +263,107 @@ module Kward
|
|
|
218
263
|
run_prompt_or_interactive
|
|
219
264
|
end
|
|
220
265
|
|
|
266
|
+
def edit_file_command(path)
|
|
267
|
+
setup_interactive_prompt
|
|
268
|
+
unless @prompt.respond_to?(:edit_file)
|
|
269
|
+
raise ArgumentError, "The integrated editor requires an interactive terminal."
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
@prompt.edit_file(path, base_dir: Dir.pwd, allow_new: true)
|
|
273
|
+
ensure
|
|
274
|
+
@prompt.close if @prompt.respond_to?(:close) && prompt_interface?
|
|
275
|
+
end
|
|
276
|
+
|
|
221
277
|
def run_prompt_or_interactive
|
|
278
|
+
stdin_input = read_stdin_input
|
|
222
279
|
first_prompt = one_shot_prompt_argument
|
|
223
|
-
if first_prompt
|
|
224
|
-
answer = one_shot(first_prompt)
|
|
225
|
-
puts answer unless answer.empty?
|
|
226
|
-
return
|
|
227
|
-
end
|
|
228
280
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
281
|
+
case resolved_execution_mode(first_prompt: first_prompt, stdin_input: stdin_input)
|
|
282
|
+
when "chat"
|
|
283
|
+
interactive_loop
|
|
284
|
+
when "filter"
|
|
285
|
+
raise ArgumentError, "Filter mode requires stdin input." if stdin_input.nil?
|
|
286
|
+
|
|
287
|
+
answer = one_shot(filter_prompt(instruction: first_prompt, input: stdin_input), filter: true)
|
|
288
|
+
puts answer unless answer.empty?
|
|
289
|
+
when "oneshot"
|
|
290
|
+
input = first_prompt || stdin_input.to_s.strip
|
|
291
|
+
answer = one_shot(input)
|
|
232
292
|
puts answer unless answer.empty?
|
|
233
|
-
return
|
|
234
293
|
end
|
|
235
|
-
|
|
236
|
-
interactive_loop
|
|
237
294
|
end
|
|
238
295
|
|
|
239
|
-
def one_shot(input)
|
|
296
|
+
def one_shot(input, filter: false)
|
|
240
297
|
streamed = false
|
|
241
298
|
assistant_streamed = false
|
|
242
299
|
markdown_chunks = []
|
|
243
300
|
conversation = new_conversation
|
|
301
|
+
apply_filter_system_prompt(conversation) if filter
|
|
244
302
|
agent = Agent.new(
|
|
245
303
|
client: @client,
|
|
246
304
|
tool_registry: ToolRegistry.new(workspace: configured_workspace, prompt: @prompt),
|
|
247
305
|
conversation: conversation
|
|
248
306
|
)
|
|
249
|
-
answer =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
307
|
+
answer = if filter
|
|
308
|
+
agent.ask(input)
|
|
309
|
+
else
|
|
310
|
+
agent.ask(input) do |event|
|
|
311
|
+
result = render_blocking_turn_event(event, markdown_chunks)
|
|
312
|
+
streamed = true if result
|
|
313
|
+
assistant_streamed = true if result == :assistant_streamed
|
|
314
|
+
end
|
|
253
315
|
end
|
|
254
316
|
flush_markdown_deltas(markdown_chunks) if streamed
|
|
317
|
+
return answer if filter
|
|
318
|
+
|
|
255
319
|
assistant_streamed ? "" : render_markdown_transcript(answer)
|
|
256
320
|
end
|
|
257
321
|
|
|
322
|
+
def resolved_execution_mode(first_prompt:, stdin_input:)
|
|
323
|
+
return @requested_mode unless @requested_mode == "auto"
|
|
324
|
+
return "chat" if stdin_input.nil? && first_prompt.nil?
|
|
325
|
+
return "filter" if !stdin_input.nil? && first_prompt
|
|
258
326
|
|
|
327
|
+
"oneshot"
|
|
328
|
+
end
|
|
259
329
|
|
|
330
|
+
def filter_prompt(instruction:, input:)
|
|
331
|
+
<<~PROMPT
|
|
332
|
+
Instruction:
|
|
333
|
+
#{instruction}
|
|
260
334
|
|
|
335
|
+
Input:
|
|
336
|
+
#{input}
|
|
337
|
+
PROMPT
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def apply_filter_system_prompt(conversation)
|
|
341
|
+
return unless conversation.system_message
|
|
261
342
|
|
|
343
|
+
conversation.system_message[:content] = [conversation.system_message[:content], filter_system_prompt].compact.join("\n\n")
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def filter_system_prompt
|
|
347
|
+
<<~PROMPT.strip
|
|
348
|
+
You are being used as a command-line text filter.
|
|
349
|
+
|
|
350
|
+
Transform the provided input according to the user's instruction.
|
|
351
|
+
Return only the transformed output.
|
|
352
|
+
|
|
353
|
+
Do not include explanations, introductions, summaries, Markdown fences, or commentary.
|
|
354
|
+
Do not say what you changed.
|
|
355
|
+
Preserve the input format unless the instruction requires changing it.
|
|
356
|
+
If the input is code, data, markup, or configuration, output only the resulting code/data/markup/configuration.
|
|
357
|
+
PROMPT
|
|
358
|
+
end
|
|
262
359
|
|
|
263
360
|
def interactive_loop(agent: nil)
|
|
264
361
|
setup_interactive_prompt
|
|
265
362
|
session_store = interactive_session_store(agent)
|
|
266
363
|
@resumed_last_session = false
|
|
267
|
-
if session_store &&
|
|
364
|
+
if session_store && @prompt.respond_to?(:update_tabs)
|
|
365
|
+
agent = setup_interactive_tabs(session_store, agent)
|
|
366
|
+
elsif session_store && agent.nil?
|
|
268
367
|
agent = resume_last_session(session_store) || build_new_session_agent(session_store)
|
|
269
368
|
elsif session_store
|
|
270
369
|
@active_session = track_session(session_store.create(provider: current_model_provider, model: current_model_id, reasoning_effort: current_reasoning_effort))
|
|
@@ -277,13 +376,29 @@ module Kward
|
|
|
277
376
|
update_assistant_prompt(agent.conversation)
|
|
278
377
|
@footer_conversation = agent.conversation
|
|
279
378
|
|
|
280
|
-
print_visual_banner unless @resumed_last_session
|
|
379
|
+
print_visual_banner unless @resumed_last_session || @restored_tabs
|
|
281
380
|
render_resumed_last_session_transcript(agent.conversation) if @resumed_last_session
|
|
282
381
|
|
|
283
382
|
@pending_inputs = []
|
|
284
383
|
|
|
285
384
|
loop do
|
|
286
|
-
|
|
385
|
+
if @pending_inputs.empty? && active_tab&.shell
|
|
386
|
+
run_ekwsh_loop(active_tab.shell, tab: active_tab)
|
|
387
|
+
end
|
|
388
|
+
input = @pending_inputs.shift || (active_tab ? poll_active_tab_input : @prompt.ask("You>"))
|
|
389
|
+
if input.is_a?(Hash) && input[:tab_action]
|
|
390
|
+
tab_result = handle_tab_action(input, session_store)
|
|
391
|
+
break if tab_result == PromptInterface::EXIT_INPUT
|
|
392
|
+
agent = active_tab.agent if active_tab
|
|
393
|
+
next
|
|
394
|
+
end
|
|
395
|
+
if input.is_a?(Hash) && input[:reasoning_action]
|
|
396
|
+
conversation = active_tab ? active_tab.agent.conversation : agent.conversation
|
|
397
|
+
cycle_reasoning(conversation, direction: input[:reasoning_action], persist: :debounced)
|
|
398
|
+
agent = active_tab.agent if active_tab
|
|
399
|
+
next
|
|
400
|
+
end
|
|
401
|
+
next if input == :tab_idle
|
|
287
402
|
break if input.nil?
|
|
288
403
|
|
|
289
404
|
display_input = submitted_display_input(input)
|
|
@@ -301,31 +416,57 @@ module Kward
|
|
|
301
416
|
end
|
|
302
417
|
break if ["/exit", "/quit"].include?(command)
|
|
303
418
|
handled, replacement_agent = handle_local_slash_command(command, agent, session_store)
|
|
304
|
-
|
|
419
|
+
if replacement_agent?(replacement_agent)
|
|
420
|
+
agent = active_tab ? replace_active_tab_agent(replacement_agent) : replacement_agent
|
|
421
|
+
end
|
|
305
422
|
end
|
|
306
423
|
next if handled
|
|
424
|
+
request_handled, request_replacement = handle_request_worker_input(command_input, agent, session_store)
|
|
425
|
+
if request_handled
|
|
426
|
+
if replacement_agent?(request_replacement)
|
|
427
|
+
agent = active_tab ? replace_active_tab_agent(request_replacement) : request_replacement
|
|
428
|
+
end
|
|
429
|
+
next
|
|
430
|
+
end
|
|
307
431
|
next if shell_command_input?(command_input) && handle_interactive_shell_command(command_input, agent)
|
|
308
432
|
|
|
433
|
+
flush_pending_reasoning_config(conversation: agent.conversation)
|
|
309
434
|
expanded_input = expand_prompt_template(input)
|
|
310
435
|
display_input = display_input || input if expanded_input
|
|
311
436
|
input = expanded_input || input
|
|
437
|
+
agent = refresh_implementation_writer(agent)
|
|
312
438
|
@footer_conversation = agent.conversation
|
|
313
439
|
begin
|
|
314
440
|
@rewind_return_leaf_id = nil
|
|
315
441
|
auto_name_active_session(display_input || input)
|
|
316
|
-
|
|
442
|
+
@foreground_turn_active = true if @active_worker_role == "implementation"
|
|
443
|
+
if active_tab
|
|
444
|
+
submit_tab_input(active_tab, input, display_input: display_input)
|
|
445
|
+
pending_inputs = []
|
|
446
|
+
else
|
|
447
|
+
pending_inputs = run_interactive_turn(agent, input, display_input: display_input)
|
|
448
|
+
agent = @busy_replacement_agent if replacement_agent?(@busy_replacement_agent)
|
|
449
|
+
@busy_replacement_agent = nil
|
|
450
|
+
end
|
|
317
451
|
pending_inputs.reverse_each { |pending_input| @pending_inputs.unshift(pending_input) }
|
|
318
452
|
rescue StandardError => e
|
|
319
453
|
runtime_output("Error: #{e.message}")
|
|
454
|
+
ensure
|
|
455
|
+
@foreground_turn_active = false if @active_worker_role == "implementation"
|
|
456
|
+
release_implementation_writer if @active_worker_role == "implementation"
|
|
320
457
|
end
|
|
321
458
|
end
|
|
322
459
|
|
|
460
|
+
flush_pending_reasoning_config(conversation: agent.conversation)
|
|
323
461
|
agent.conversation
|
|
324
462
|
rescue Interrupt
|
|
463
|
+
flush_pending_reasoning_config(conversation: agent&.conversation)
|
|
325
464
|
runtime_output("Goodbye.")
|
|
326
465
|
agent&.conversation
|
|
327
466
|
ensure
|
|
328
467
|
begin
|
|
468
|
+
stop_tabs if respond_to?(:stop_tabs, true)
|
|
469
|
+
stop_live_worker_view if respond_to?(:stop_live_worker_view, true)
|
|
329
470
|
@prompt.close if prompt_interface?
|
|
330
471
|
ensure
|
|
331
472
|
cleanup_unused_sessions
|
|
@@ -334,9 +475,13 @@ module Kward
|
|
|
334
475
|
end
|
|
335
476
|
|
|
336
477
|
def piped_prompt
|
|
337
|
-
|
|
478
|
+
read_stdin_input.to_s.strip
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def read_stdin_input
|
|
482
|
+
return nil if @stdin.tty?
|
|
338
483
|
|
|
339
|
-
@stdin.read
|
|
484
|
+
@stdin.read
|
|
340
485
|
end
|
|
341
486
|
|
|
342
487
|
end
|
data/lib/kward/compactor.rb
CHANGED
|
@@ -665,7 +665,10 @@ module Kward
|
|
|
665
665
|
## Critical Context
|
|
666
666
|
- [Preserve important context, add new context needed to continue]
|
|
667
667
|
|
|
668
|
-
|
|
668
|
+
## Available Tool Artifacts
|
|
669
|
+
- [Preserve any toolout_* ids from compacted tool outputs, with what each id contains and why it may matter]
|
|
670
|
+
|
|
671
|
+
Keep each section concise. Preserve exact file paths, class names, module names, method names, constants, commands, spec names, migration names, error messages, toolout_* artifact ids, user requirements, and unresolved problems. Do not invent work that did not happen.
|
|
669
672
|
PROMPT
|
|
670
673
|
|
|
671
674
|
SPLIT_TURN_PROMPT = <<~PROMPT.strip.freeze
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
require "digest"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
require "json"
|
|
3
4
|
require "yaml"
|
|
4
5
|
require_relative "private_file"
|
|
6
|
+
require_relative "editor_mode"
|
|
5
7
|
require_relative "prompts/templates"
|
|
6
8
|
require_relative "skills/registry"
|
|
7
9
|
|
|
@@ -64,6 +66,10 @@ module Kward
|
|
|
64
66
|
File.join(config_dir, "cache")
|
|
65
67
|
end
|
|
66
68
|
|
|
69
|
+
def ekwsh_config_path
|
|
70
|
+
File.join(config_dir, "ekwsh.yml")
|
|
71
|
+
end
|
|
72
|
+
|
|
67
73
|
def default_config
|
|
68
74
|
{
|
|
69
75
|
"personas" => JSON.parse(JSON.generate(DEFAULT_PERSONAS)),
|
|
@@ -72,7 +78,16 @@ module Kward
|
|
|
72
78
|
"auto_summary" => false
|
|
73
79
|
},
|
|
74
80
|
"composer" => {
|
|
75
|
-
"busy_help" => true
|
|
81
|
+
"busy_help" => true,
|
|
82
|
+
"tab_keybindings" => "auto"
|
|
83
|
+
},
|
|
84
|
+
"editor" => {
|
|
85
|
+
"mode" => "modern",
|
|
86
|
+
"auto_indent" => true,
|
|
87
|
+
"auto_close_pairs" => true,
|
|
88
|
+
"soft_wrap" => true,
|
|
89
|
+
"bar_cursor" => true,
|
|
90
|
+
"line_numbers" => "absolute"
|
|
76
91
|
},
|
|
77
92
|
"sessions" => {
|
|
78
93
|
"auto_resume" => false
|
|
@@ -97,6 +112,19 @@ module Kward
|
|
|
97
112
|
File.join(cache_dir, "code_search")
|
|
98
113
|
end
|
|
99
114
|
|
|
115
|
+
def openrouter_models_cache_path
|
|
116
|
+
File.join(cache_dir, "openrouter_models.json")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def project_browser_state_path
|
|
120
|
+
File.join(cache_dir, "project_browser_state.json")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def prompt_history_path(cwd, config_dir: self.config_dir)
|
|
124
|
+
key = Digest::SHA256.hexdigest(canonical_workspace_root(cwd))[0, 24]
|
|
125
|
+
File.join(config_dir, "history", "#{key}.jsonl")
|
|
126
|
+
end
|
|
127
|
+
|
|
100
128
|
# @return [String] directory containing structured memory files
|
|
101
129
|
def memory_dir
|
|
102
130
|
File.join(config_dir, "memory")
|
|
@@ -140,6 +168,50 @@ module Kward
|
|
|
140
168
|
PrivateFile.write_json(path, config)
|
|
141
169
|
end
|
|
142
170
|
|
|
171
|
+
def read_ekwsh_config(path = ekwsh_config_path)
|
|
172
|
+
path = File.expand_path(path)
|
|
173
|
+
return { env: {}, aliases: {} } unless File.exist?(path)
|
|
174
|
+
|
|
175
|
+
data = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
|
|
176
|
+
normalize_ekwsh_config(data)
|
|
177
|
+
rescue Psych::SyntaxError => e
|
|
178
|
+
raise "Invalid ekwsh YAML config: #{path}: #{e.message}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def normalize_ekwsh_config(data)
|
|
182
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
183
|
+
settings = data.is_a?(Hash) ? data : {}
|
|
184
|
+
{
|
|
185
|
+
env: normalize_ekwsh_env(settings["env"]),
|
|
186
|
+
aliases: normalize_ekwsh_aliases(settings["aliases"])
|
|
187
|
+
}
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def normalize_ekwsh_env(values)
|
|
191
|
+
return {} unless values.is_a?(Hash)
|
|
192
|
+
|
|
193
|
+
values.each_with_object({}) do |(key, value), result|
|
|
194
|
+
key = key.to_s
|
|
195
|
+
next unless key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
196
|
+
next if value.nil?
|
|
197
|
+
|
|
198
|
+
result[key] = value.to_s
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def normalize_ekwsh_aliases(values)
|
|
203
|
+
return {} unless values.is_a?(Hash)
|
|
204
|
+
|
|
205
|
+
values.each_with_object({}) do |(name, command), result|
|
|
206
|
+
name = name.to_s
|
|
207
|
+
command = command.to_s.strip
|
|
208
|
+
next unless name.match?(/\A[A-Za-z0-9_.:-]+\z/)
|
|
209
|
+
next if command.empty?
|
|
210
|
+
|
|
211
|
+
result[name] = command
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
143
215
|
# Merges top-level config values and writes the updated config privately.
|
|
144
216
|
def update_config(values, path = config_path)
|
|
145
217
|
raise "Config values must be an object" unless values.is_a?(Hash)
|
|
@@ -150,6 +222,17 @@ module Kward
|
|
|
150
222
|
config
|
|
151
223
|
end
|
|
152
224
|
|
|
225
|
+
# Merges values into a one-level nested config section and writes privately.
|
|
226
|
+
def update_nested_config(section, values, path = config_path)
|
|
227
|
+
raise "Config values must be an object" unless values.is_a?(Hash)
|
|
228
|
+
|
|
229
|
+
config = read_config(path)
|
|
230
|
+
current = config[section.to_s].is_a?(Hash) ? config[section.to_s].dup : {}
|
|
231
|
+
config[section.to_s] = current.merge(values.transform_keys(&:to_s))
|
|
232
|
+
write_config(config, path)
|
|
233
|
+
config
|
|
234
|
+
end
|
|
235
|
+
|
|
153
236
|
# Removes a top-level config key when it exists.
|
|
154
237
|
def delete_config_key(key, path = config_path)
|
|
155
238
|
config = read_config(path)
|
|
@@ -189,10 +272,47 @@ module Kward
|
|
|
189
272
|
composer["busy_help"] != false
|
|
190
273
|
end
|
|
191
274
|
|
|
192
|
-
# Returns
|
|
193
|
-
def
|
|
194
|
-
|
|
195
|
-
|
|
275
|
+
# Returns the configured tab keybinding family, or auto when unset/invalid.
|
|
276
|
+
def composer_tab_keybindings(config = read_config)
|
|
277
|
+
composer = config["composer"].is_a?(Hash) ? config["composer"] : {}
|
|
278
|
+
value = composer["tab_keybindings"].to_s.downcase
|
|
279
|
+
%w[auto ctrl alt].include?(value) ? value : "auto"
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Returns the built-in TUI editor keymap mode.
|
|
283
|
+
def editor_mode(config = read_config)
|
|
284
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
285
|
+
EditorMode.normalize(editor["mode"])
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Returns whether the built-in TUI editor should auto-indent new lines.
|
|
289
|
+
def editor_auto_indent?(config = read_config)
|
|
290
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
291
|
+
editor["auto_indent"] != false
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Returns whether the built-in TUI editor should auto-close typed pairs.
|
|
295
|
+
def editor_auto_close_pairs?(config = read_config)
|
|
296
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
297
|
+
editor["auto_close_pairs"] != false
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Returns whether the built-in TUI editor should soft-wrap long lines.
|
|
301
|
+
def editor_soft_wrap?(config = read_config)
|
|
302
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
303
|
+
editor["soft_wrap"] != false
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Returns whether editable built-in TUI editor buffers should use a bar cursor.
|
|
307
|
+
def editor_bar_cursor?(config = read_config)
|
|
308
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
309
|
+
editor["bar_cursor"] != false
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
# Returns the built-in TUI editor line-number display mode.
|
|
313
|
+
def editor_line_numbers(config = read_config)
|
|
314
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
315
|
+
EditorMode.normalize_line_numbers(editor["line_numbers"])
|
|
196
316
|
end
|
|
197
317
|
|
|
198
318
|
# Returns whether file tools must stay inside the active workspace root.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Namespace for the Kward CLI agent runtime.
|
|
2
|
+
module Kward
|
|
3
|
+
# Tracks approximate context bytes saved by tool budgeting during one process.
|
|
4
|
+
class ContextBudgetMeter
|
|
5
|
+
Snapshot = Struct.new(:calls, :original_bytes, :returned_bytes, :saved_bytes, :tool_breakdown, keyword_init: true)
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@mutex = Mutex.new
|
|
9
|
+
@calls = 0
|
|
10
|
+
@original_bytes = 0
|
|
11
|
+
@returned_bytes = 0
|
|
12
|
+
@tool_breakdown = Hash.new { |hash, key| hash[key] = { calls: 0, originalBytes: 0, returnedBytes: 0, savedBytes: 0 } }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def record(tool_name:, original_bytes:, returned_bytes:)
|
|
16
|
+
original_bytes = original_bytes.to_i
|
|
17
|
+
returned_bytes = returned_bytes.to_i
|
|
18
|
+
saved_bytes = [original_bytes - returned_bytes, 0].max
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
@calls += 1
|
|
21
|
+
@original_bytes += original_bytes
|
|
22
|
+
@returned_bytes += returned_bytes
|
|
23
|
+
entry = @tool_breakdown[tool_name.to_s]
|
|
24
|
+
entry[:calls] += 1
|
|
25
|
+
entry[:originalBytes] += original_bytes
|
|
26
|
+
entry[:returnedBytes] += returned_bytes
|
|
27
|
+
entry[:savedBytes] += saved_bytes
|
|
28
|
+
end
|
|
29
|
+
saved_bytes
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def snapshot
|
|
33
|
+
@mutex.synchronize do
|
|
34
|
+
Snapshot.new(
|
|
35
|
+
calls: @calls,
|
|
36
|
+
original_bytes: @original_bytes,
|
|
37
|
+
returned_bytes: @returned_bytes,
|
|
38
|
+
saved_bytes: [@original_bytes - @returned_bytes, 0].max,
|
|
39
|
+
tool_breakdown: @tool_breakdown.transform_values(&:dup)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|