kward 0.71.0 → 0.73.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/ci.yml +30 -0
- data/CHANGELOG.md +93 -0
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +415 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +123 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +75 -5
- data/doc/session-management.md +35 -1
- data/doc/shell.md +332 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +79 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/ansi.rb +62 -23
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/rendering.rb +4 -1
- data/lib/kward/cli/runtime_helpers.rb +268 -4
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +217 -9
- data/lib/kward/cli/slash_commands.rb +628 -2
- data/lib/kward/cli/tabs.rb +725 -0
- data/lib/kward/cli/tool_summaries.rb +6 -0
- data/lib/kward/cli.rb +150 -26
- data/lib/kward/clipboard.rb +2 -3
- data/lib/kward/compactor.rb +7 -19
- data/lib/kward/config_files.rb +145 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +559 -0
- data/lib/kward/image_attachments.rb +3 -1
- data/lib/kward/interactive_pty_runner.rb +151 -0
- data/lib/kward/local_command_runner.rb +155 -0
- data/lib/kward/local_pty_command_runner.rb +171 -0
- data/lib/kward/model/context_usage.rb +2 -2
- data/lib/kward/model/payloads.rb +2 -5
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +84 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +510 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1218 -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 +354 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
- data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +244 -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 +1271 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -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 +288 -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 +451 -57
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +99 -56
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +19 -3
- data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
- data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +366 -222
- data/lib/kward/prompts/commands.rb +9 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/memory_methods.rb +83 -0
- data/lib/kward/rpc/server.rb +169 -83
- data/lib/kward/rpc/session_manager.rb +45 -121
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/rpc/tool_metadata.rb +11 -0
- data/lib/kward/rpc/transcript_normalizer.rb +4 -39
- data/lib/kward/scratchpad_runner.rb +56 -0
- data/lib/kward/session_diff.rb +20 -3
- data/lib/kward/session_naming.rb +11 -0
- data/lib/kward/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/terminal_keys.rb +84 -0
- data/lib/kward/terminal_sequences.rb +42 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +204 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +93 -0
- data/lib/kward/workers/job.rb +99 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/queue_runner.rb +166 -0
- data/lib/kward/workers/queue_store.rb +112 -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 +10 -0
- data/lib/kward/workspace.rb +125 -87
- data/templates/default/fulldoc/html/css/kward.css +140 -36
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
- data/templates/default/fulldoc/html/setup.rb +1 -0
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +23 -34
- data/templates/default/layout/html/setup.rb +6 -0
- metadata +67 -1
|
@@ -21,6 +21,8 @@ module Kward
|
|
|
21
21
|
shell_command_summary(args, text)
|
|
22
22
|
when "web_search"
|
|
23
23
|
web_search_summary(args, text)
|
|
24
|
+
when "read_skill"
|
|
25
|
+
read_skill_summary(text)
|
|
24
26
|
else
|
|
25
27
|
generic_tool_summary(name, text)
|
|
26
28
|
end
|
|
@@ -76,6 +78,10 @@ module Kward
|
|
|
76
78
|
lines.join("\n")
|
|
77
79
|
end
|
|
78
80
|
|
|
81
|
+
def read_skill_summary(content)
|
|
82
|
+
"read_skill:\n#{content}"
|
|
83
|
+
end
|
|
84
|
+
|
|
79
85
|
def error_tool_summary(name, args, content)
|
|
80
86
|
path = args["path"] || args[:path]
|
|
81
87
|
command = args["command"] || args[:command]
|
data/lib/kward/cli.rb
CHANGED
|
@@ -9,10 +9,13 @@ require_relative "model/client"
|
|
|
9
9
|
require_relative "compactor"
|
|
10
10
|
require_relative "config_files"
|
|
11
11
|
require_relative "clipboard"
|
|
12
|
+
require_relative "cancellation"
|
|
12
13
|
require_relative "cli_transcript_formatter"
|
|
13
14
|
require_relative "model/context_usage"
|
|
14
15
|
require_relative "events"
|
|
15
16
|
require_relative "export_path"
|
|
17
|
+
require_relative "ekwsh"
|
|
18
|
+
require_relative "interactive_pty_runner"
|
|
16
19
|
require_relative "auth/anthropic_oauth"
|
|
17
20
|
require_relative "auth/github_oauth"
|
|
18
21
|
require_relative "auth/openrouter_api_key"
|
|
@@ -29,10 +32,13 @@ require_relative "model/retry_message"
|
|
|
29
32
|
require_relative "rpc/server"
|
|
30
33
|
require_relative "session_diff"
|
|
31
34
|
require_relative "session_store"
|
|
35
|
+
require_relative "session_naming"
|
|
36
|
+
require_relative "tab_store"
|
|
32
37
|
require_relative "session_trash"
|
|
33
38
|
require_relative "session_tree_renderer"
|
|
34
39
|
require_relative "starter_pack_installer"
|
|
35
40
|
require_relative "steering"
|
|
41
|
+
require_relative "workers"
|
|
36
42
|
require_relative "tools/tool_call"
|
|
37
43
|
require_relative "tools/registry"
|
|
38
44
|
require_relative "telemetry/stats"
|
|
@@ -48,10 +54,12 @@ require_relative "cli/slash_commands"
|
|
|
48
54
|
require_relative "cli/memory_commands"
|
|
49
55
|
require_relative "cli/settings"
|
|
50
56
|
require_relative "cli/sessions"
|
|
57
|
+
require_relative "cli/tabs"
|
|
51
58
|
require_relative "cli/compaction"
|
|
52
59
|
require_relative "cli/rendering"
|
|
53
60
|
require_relative "cli/prompt_interface"
|
|
54
61
|
require_relative "cli/plugins"
|
|
62
|
+
require_relative "cli/git"
|
|
55
63
|
require_relative "cli/interactive_turn"
|
|
56
64
|
require_relative "cli/tool_summaries"
|
|
57
65
|
|
|
@@ -60,8 +68,6 @@ module Kward
|
|
|
60
68
|
# Command-line interface for interactive chat, one-shot prompts, login,
|
|
61
69
|
# telemetry export, Pan server mode, and the experimental JSON-RPC backend.
|
|
62
70
|
class CLI
|
|
63
|
-
# Order from the captain: The next line shall never be deleted.
|
|
64
|
-
STATUS_MESSAGE = "This is a totally important status message about a non-existing status. Hi ChatGPT 👋"
|
|
65
71
|
RESTORED_TOOL_OUTPUT_LIMIT = 2_000
|
|
66
72
|
INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT = 10
|
|
67
73
|
STREAM_RENDER_INTERVAL = 0.025
|
|
@@ -80,10 +86,12 @@ module Kward
|
|
|
80
86
|
include CLI::MemoryCommands
|
|
81
87
|
include CLI::Settings
|
|
82
88
|
include CLI::Sessions
|
|
89
|
+
include CLI::Tabs
|
|
83
90
|
include CLI::CompactionCommands
|
|
84
91
|
include CLI::Rendering
|
|
85
92
|
include CLI::PromptInterfaceSupport
|
|
86
93
|
include CLI::Plugins
|
|
94
|
+
include CLI::GitCommands
|
|
87
95
|
include CLI::InteractiveTurn
|
|
88
96
|
include CLI::ToolSummaries
|
|
89
97
|
|
|
@@ -100,6 +108,11 @@ module Kward
|
|
|
100
108
|
@plugin_registry = nil
|
|
101
109
|
@working_directory = nil
|
|
102
110
|
@prompt_delimited = false
|
|
111
|
+
@requested_mode = "auto"
|
|
112
|
+
@experimental_workers = false
|
|
113
|
+
@foreground_turn_active = false
|
|
114
|
+
@pending_reasoning_config = nil
|
|
115
|
+
@pending_reasoning_config_mutex = Mutex.new
|
|
103
116
|
@color_enabled = ANSI.enabled?($stdout)
|
|
104
117
|
end
|
|
105
118
|
|
|
@@ -164,6 +177,17 @@ module Kward
|
|
|
164
177
|
return
|
|
165
178
|
end
|
|
166
179
|
|
|
180
|
+
if @argv.first == "edit"
|
|
181
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
182
|
+
print_command_help("edit")
|
|
183
|
+
return
|
|
184
|
+
end
|
|
185
|
+
raise ArgumentError, command_usage("edit") unless @argv.length == 2
|
|
186
|
+
|
|
187
|
+
edit_file_command(@argv[1])
|
|
188
|
+
return
|
|
189
|
+
end
|
|
190
|
+
|
|
167
191
|
if @argv.first == "sysprompt"
|
|
168
192
|
if help_option_arguments?(@argv[1..] || [])
|
|
169
193
|
print_command_help("sysprompt")
|
|
@@ -181,7 +205,7 @@ module Kward
|
|
|
181
205
|
end
|
|
182
206
|
raise ArgumentError, command_usage("rpc") unless @argv.length == 1
|
|
183
207
|
|
|
184
|
-
Kward::RPC::Server.new(input: @stdin, output: $stdout, client: @client).run
|
|
208
|
+
Kward::RPC::Server.new(input: @stdin, output: $stdout, client: @client, experimental_workers: @experimental_workers).run
|
|
185
209
|
return
|
|
186
210
|
end
|
|
187
211
|
|
|
@@ -231,53 +255,107 @@ module Kward
|
|
|
231
255
|
run_prompt_or_interactive
|
|
232
256
|
end
|
|
233
257
|
|
|
258
|
+
def edit_file_command(path)
|
|
259
|
+
setup_interactive_prompt
|
|
260
|
+
unless @prompt.respond_to?(:edit_file)
|
|
261
|
+
raise ArgumentError, "The integrated editor requires an interactive terminal."
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
@prompt.edit_file(path, base_dir: Dir.pwd, allow_new: true)
|
|
265
|
+
ensure
|
|
266
|
+
@prompt.close if @prompt.respond_to?(:close) && prompt_interface?
|
|
267
|
+
end
|
|
268
|
+
|
|
234
269
|
def run_prompt_or_interactive
|
|
270
|
+
stdin_input = read_stdin_input
|
|
235
271
|
first_prompt = one_shot_prompt_argument
|
|
236
|
-
if first_prompt
|
|
237
|
-
answer = one_shot(first_prompt)
|
|
238
|
-
puts answer unless answer.empty?
|
|
239
|
-
return
|
|
240
|
-
end
|
|
241
272
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
273
|
+
case resolved_execution_mode(first_prompt: first_prompt, stdin_input: stdin_input)
|
|
274
|
+
when "chat"
|
|
275
|
+
interactive_loop
|
|
276
|
+
when "filter"
|
|
277
|
+
raise ArgumentError, "Filter mode requires stdin input." if stdin_input.nil?
|
|
278
|
+
|
|
279
|
+
answer = one_shot(filter_prompt(instruction: first_prompt, input: stdin_input), filter: true)
|
|
280
|
+
puts answer unless answer.empty?
|
|
281
|
+
when "oneshot"
|
|
282
|
+
input = first_prompt || stdin_input.to_s.strip
|
|
283
|
+
answer = one_shot(input)
|
|
245
284
|
puts answer unless answer.empty?
|
|
246
|
-
return
|
|
247
285
|
end
|
|
248
|
-
|
|
249
|
-
interactive_loop
|
|
250
286
|
end
|
|
251
287
|
|
|
252
|
-
def one_shot(input)
|
|
288
|
+
def one_shot(input, filter: false)
|
|
253
289
|
streamed = false
|
|
254
290
|
assistant_streamed = false
|
|
255
291
|
markdown_chunks = []
|
|
256
292
|
conversation = new_conversation
|
|
293
|
+
apply_filter_system_prompt(conversation) if filter
|
|
257
294
|
agent = Agent.new(
|
|
258
295
|
client: @client,
|
|
259
296
|
tool_registry: ToolRegistry.new(workspace: configured_workspace, prompt: @prompt),
|
|
260
297
|
conversation: conversation
|
|
261
298
|
)
|
|
262
|
-
answer =
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
299
|
+
answer = if filter
|
|
300
|
+
agent.ask(input)
|
|
301
|
+
else
|
|
302
|
+
agent.ask(input) do |event|
|
|
303
|
+
result = render_blocking_turn_event(event, markdown_chunks)
|
|
304
|
+
streamed = true if result
|
|
305
|
+
assistant_streamed = true if result == :assistant_streamed
|
|
306
|
+
end
|
|
266
307
|
end
|
|
267
308
|
flush_markdown_deltas(markdown_chunks) if streamed
|
|
309
|
+
return answer if filter
|
|
310
|
+
|
|
268
311
|
assistant_streamed ? "" : render_markdown_transcript(answer)
|
|
269
312
|
end
|
|
270
313
|
|
|
314
|
+
def resolved_execution_mode(first_prompt:, stdin_input:)
|
|
315
|
+
return @requested_mode unless @requested_mode == "auto"
|
|
316
|
+
return "chat" if stdin_input.nil? && first_prompt.nil?
|
|
317
|
+
return "filter" if !stdin_input.nil? && first_prompt
|
|
318
|
+
|
|
319
|
+
"oneshot"
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def filter_prompt(instruction:, input:)
|
|
323
|
+
<<~PROMPT
|
|
324
|
+
Instruction:
|
|
325
|
+
#{instruction}
|
|
326
|
+
|
|
327
|
+
Input:
|
|
328
|
+
#{input}
|
|
329
|
+
PROMPT
|
|
330
|
+
end
|
|
271
331
|
|
|
332
|
+
def apply_filter_system_prompt(conversation)
|
|
333
|
+
return unless conversation.system_message
|
|
334
|
+
|
|
335
|
+
conversation.system_message[:content] = [conversation.system_message[:content], filter_system_prompt].compact.join("\n\n")
|
|
336
|
+
end
|
|
272
337
|
|
|
338
|
+
def filter_system_prompt
|
|
339
|
+
<<~PROMPT.strip
|
|
340
|
+
You are being used as a command-line text filter.
|
|
273
341
|
|
|
342
|
+
Transform the provided input according to the user's instruction.
|
|
343
|
+
Return only the transformed output.
|
|
274
344
|
|
|
345
|
+
Do not include explanations, introductions, summaries, Markdown fences, or commentary.
|
|
346
|
+
Do not say what you changed.
|
|
347
|
+
Preserve the input format unless the instruction requires changing it.
|
|
348
|
+
If the input is code, data, markup, or configuration, output only the resulting code/data/markup/configuration.
|
|
349
|
+
PROMPT
|
|
350
|
+
end
|
|
275
351
|
|
|
276
352
|
def interactive_loop(agent: nil)
|
|
277
353
|
setup_interactive_prompt
|
|
278
354
|
session_store = interactive_session_store(agent)
|
|
279
355
|
@resumed_last_session = false
|
|
280
|
-
if session_store &&
|
|
356
|
+
if session_store && @prompt.respond_to?(:update_tabs)
|
|
357
|
+
agent = setup_interactive_tabs(session_store, agent)
|
|
358
|
+
elsif session_store && agent.nil?
|
|
281
359
|
agent = resume_last_session(session_store) || build_new_session_agent(session_store)
|
|
282
360
|
elsif session_store
|
|
283
361
|
@active_session = track_session(session_store.create(provider: current_model_provider, model: current_model_id, reasoning_effort: current_reasoning_effort))
|
|
@@ -290,13 +368,29 @@ module Kward
|
|
|
290
368
|
update_assistant_prompt(agent.conversation)
|
|
291
369
|
@footer_conversation = agent.conversation
|
|
292
370
|
|
|
293
|
-
print_visual_banner unless @resumed_last_session
|
|
371
|
+
print_visual_banner unless @resumed_last_session || @restored_tabs
|
|
294
372
|
render_resumed_last_session_transcript(agent.conversation) if @resumed_last_session
|
|
295
373
|
|
|
296
374
|
@pending_inputs = []
|
|
297
375
|
|
|
298
376
|
loop do
|
|
299
|
-
|
|
377
|
+
if @pending_inputs.empty? && active_tab&.shell
|
|
378
|
+
run_ekwsh_loop(active_tab.shell, tab: active_tab, history: build_ekwsh_history(active_tab.agent))
|
|
379
|
+
end
|
|
380
|
+
input = @pending_inputs.shift || (active_tab ? poll_active_tab_input : @prompt.ask("You>"))
|
|
381
|
+
if input.is_a?(Hash) && input[:tab_action]
|
|
382
|
+
tab_result = handle_tab_action(input, session_store)
|
|
383
|
+
break if tab_result == PromptInterface::EXIT_INPUT
|
|
384
|
+
agent = active_tab.agent if active_tab
|
|
385
|
+
next
|
|
386
|
+
end
|
|
387
|
+
if input.is_a?(Hash) && input[:reasoning_action]
|
|
388
|
+
conversation = active_tab ? active_tab.agent.conversation : agent.conversation
|
|
389
|
+
cycle_reasoning(conversation, direction: input[:reasoning_action], persist: :debounced)
|
|
390
|
+
agent = active_tab.agent if active_tab
|
|
391
|
+
next
|
|
392
|
+
end
|
|
393
|
+
next if input == :tab_idle
|
|
300
394
|
break if input.nil?
|
|
301
395
|
|
|
302
396
|
display_input = submitted_display_input(input)
|
|
@@ -314,31 +408,57 @@ module Kward
|
|
|
314
408
|
end
|
|
315
409
|
break if ["/exit", "/quit"].include?(command)
|
|
316
410
|
handled, replacement_agent = handle_local_slash_command(command, agent, session_store)
|
|
317
|
-
|
|
411
|
+
if replacement_agent?(replacement_agent)
|
|
412
|
+
agent = active_tab ? replace_active_tab_agent(replacement_agent) : replacement_agent
|
|
413
|
+
end
|
|
318
414
|
end
|
|
319
415
|
next if handled
|
|
416
|
+
request_handled, request_replacement = handle_request_worker_input(command_input, agent, session_store)
|
|
417
|
+
if request_handled
|
|
418
|
+
if replacement_agent?(request_replacement)
|
|
419
|
+
agent = active_tab ? replace_active_tab_agent(request_replacement) : request_replacement
|
|
420
|
+
end
|
|
421
|
+
next
|
|
422
|
+
end
|
|
320
423
|
next if shell_command_input?(command_input) && handle_interactive_shell_command(command_input, agent)
|
|
321
424
|
|
|
425
|
+
flush_pending_reasoning_config(conversation: agent.conversation)
|
|
322
426
|
expanded_input = expand_prompt_template(input)
|
|
323
427
|
display_input = display_input || input if expanded_input
|
|
324
428
|
input = expanded_input || input
|
|
429
|
+
agent = refresh_implementation_writer(agent)
|
|
325
430
|
@footer_conversation = agent.conversation
|
|
326
431
|
begin
|
|
327
432
|
@rewind_return_leaf_id = nil
|
|
328
433
|
auto_name_active_session(display_input || input)
|
|
329
|
-
|
|
434
|
+
@foreground_turn_active = true if @active_worker_role == "implementation"
|
|
435
|
+
if active_tab
|
|
436
|
+
submit_tab_input(active_tab, input, display_input: display_input)
|
|
437
|
+
pending_inputs = []
|
|
438
|
+
else
|
|
439
|
+
pending_inputs = run_interactive_turn(agent, input, display_input: display_input)
|
|
440
|
+
agent = @busy_replacement_agent if replacement_agent?(@busy_replacement_agent)
|
|
441
|
+
@busy_replacement_agent = nil
|
|
442
|
+
end
|
|
330
443
|
pending_inputs.reverse_each { |pending_input| @pending_inputs.unshift(pending_input) }
|
|
331
444
|
rescue StandardError => e
|
|
332
445
|
runtime_output("Error: #{e.message}")
|
|
446
|
+
ensure
|
|
447
|
+
@foreground_turn_active = false if @active_worker_role == "implementation"
|
|
448
|
+
release_implementation_writer if @active_worker_role == "implementation"
|
|
333
449
|
end
|
|
334
450
|
end
|
|
335
451
|
|
|
452
|
+
flush_pending_reasoning_config(conversation: agent.conversation)
|
|
336
453
|
agent.conversation
|
|
337
454
|
rescue Interrupt
|
|
455
|
+
flush_pending_reasoning_config(conversation: agent&.conversation)
|
|
338
456
|
runtime_output("Goodbye.")
|
|
339
457
|
agent&.conversation
|
|
340
458
|
ensure
|
|
341
459
|
begin
|
|
460
|
+
stop_tabs if respond_to?(:stop_tabs, true)
|
|
461
|
+
stop_live_worker_view if respond_to?(:stop_live_worker_view, true)
|
|
342
462
|
@prompt.close if prompt_interface?
|
|
343
463
|
ensure
|
|
344
464
|
cleanup_unused_sessions
|
|
@@ -347,9 +467,13 @@ module Kward
|
|
|
347
467
|
end
|
|
348
468
|
|
|
349
469
|
def piped_prompt
|
|
350
|
-
|
|
470
|
+
read_stdin_input.to_s.strip
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def read_stdin_input
|
|
474
|
+
return nil if @stdin.tty?
|
|
351
475
|
|
|
352
|
-
@stdin.read
|
|
476
|
+
@stdin.read
|
|
353
477
|
end
|
|
354
478
|
|
|
355
479
|
end
|
data/lib/kward/clipboard.rb
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
require "base64"
|
|
2
1
|
require "open3"
|
|
2
|
+
require_relative "terminal_sequences"
|
|
3
3
|
require "rbconfig"
|
|
4
4
|
|
|
5
5
|
# Namespace for the Kward CLI agent runtime.
|
|
@@ -39,8 +39,7 @@ module Kward
|
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
def write_osc52(content)
|
|
42
|
-
|
|
43
|
-
@output.print("\e]52;c;#{encoded}\a")
|
|
42
|
+
@output.print(TerminalSequences.osc52(content))
|
|
44
43
|
@output.flush if @output.respond_to?(:flush)
|
|
45
44
|
end
|
|
46
45
|
|
data/lib/kward/compactor.rb
CHANGED
|
@@ -130,8 +130,7 @@ module Kward
|
|
|
130
130
|
end
|
|
131
131
|
|
|
132
132
|
def tool_calls(message)
|
|
133
|
-
|
|
134
|
-
calls.is_a?(Array) ? calls : []
|
|
133
|
+
MessageAccess.tool_calls(message)
|
|
135
134
|
end
|
|
136
135
|
|
|
137
136
|
def tool_call_name(tool_call)
|
|
@@ -335,7 +334,7 @@ module Kward
|
|
|
335
334
|
|
|
336
335
|
def tool_call_args(tool_call)
|
|
337
336
|
function = tool_call["function"] || tool_call[:function] || {}
|
|
338
|
-
|
|
337
|
+
ToolCall.parse_arguments(function["arguments"] || function[:arguments])
|
|
339
338
|
end
|
|
340
339
|
|
|
341
340
|
def tool_command(tool_call)
|
|
@@ -351,15 +350,6 @@ module Kward
|
|
|
351
350
|
"#{name}(#{rendered})"
|
|
352
351
|
end
|
|
353
352
|
end
|
|
354
|
-
|
|
355
|
-
def parse_tool_arguments(arguments)
|
|
356
|
-
return {} if arguments.nil? || arguments.empty?
|
|
357
|
-
return arguments if arguments.is_a?(Hash)
|
|
358
|
-
|
|
359
|
-
JSON.parse(arguments)
|
|
360
|
-
rescue JSON::ParserError
|
|
361
|
-
{}
|
|
362
|
-
end
|
|
363
353
|
end
|
|
364
354
|
|
|
365
355
|
# Compaction support object used by conversation summarization.
|
|
@@ -441,7 +431,7 @@ module Kward
|
|
|
441
431
|
end
|
|
442
432
|
|
|
443
433
|
def message_role(message)
|
|
444
|
-
|
|
434
|
+
MessageAccess.role(message)
|
|
445
435
|
end
|
|
446
436
|
end
|
|
447
437
|
|
|
@@ -518,22 +508,20 @@ module Kward
|
|
|
518
508
|
end
|
|
519
509
|
|
|
520
510
|
def compaction_summary(message)
|
|
521
|
-
|
|
511
|
+
MessageAccess.summary(message) || MessageAccess.content(message)
|
|
522
512
|
end
|
|
523
513
|
|
|
524
514
|
def compaction_details(message)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
details = message["details"] || message[:details]
|
|
515
|
+
details = MessageAccess.value(message, :details)
|
|
528
516
|
details.is_a?(Hash) ? details : {}
|
|
529
517
|
end
|
|
530
518
|
|
|
531
519
|
def entry_id(message, index)
|
|
532
|
-
message
|
|
520
|
+
MessageAccess.value(message, :id) || "message:#{index}"
|
|
533
521
|
end
|
|
534
522
|
|
|
535
523
|
def message_role(message)
|
|
536
|
-
|
|
524
|
+
MessageAccess.role(message)
|
|
537
525
|
end
|
|
538
526
|
end
|
|
539
527
|
|
data/lib/kward/config_files.rb
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
require "digest"
|
|
1
2
|
require "fileutils"
|
|
2
3
|
require "json"
|
|
3
4
|
require "yaml"
|
|
4
5
|
require_relative "private_file"
|
|
6
|
+
require_relative "ekwsh"
|
|
7
|
+
require_relative "editor_mode"
|
|
5
8
|
require_relative "prompts/templates"
|
|
6
9
|
require_relative "skills/registry"
|
|
7
10
|
|
|
@@ -64,6 +67,10 @@ module Kward
|
|
|
64
67
|
File.join(config_dir, "cache")
|
|
65
68
|
end
|
|
66
69
|
|
|
70
|
+
def ekwsh_config_path
|
|
71
|
+
File.join(config_dir, "ekwsh.yml")
|
|
72
|
+
end
|
|
73
|
+
|
|
67
74
|
def default_config
|
|
68
75
|
{
|
|
69
76
|
"personas" => JSON.parse(JSON.generate(DEFAULT_PERSONAS)),
|
|
@@ -72,7 +79,16 @@ module Kward
|
|
|
72
79
|
"auto_summary" => false
|
|
73
80
|
},
|
|
74
81
|
"composer" => {
|
|
75
|
-
"busy_help" => true
|
|
82
|
+
"busy_help" => true,
|
|
83
|
+
"tab_keybindings" => "auto"
|
|
84
|
+
},
|
|
85
|
+
"editor" => {
|
|
86
|
+
"mode" => "modern",
|
|
87
|
+
"auto_indent" => true,
|
|
88
|
+
"auto_close_pairs" => true,
|
|
89
|
+
"soft_wrap" => true,
|
|
90
|
+
"bar_cursor" => true,
|
|
91
|
+
"line_numbers" => "absolute"
|
|
76
92
|
},
|
|
77
93
|
"sessions" => {
|
|
78
94
|
"auto_resume" => false
|
|
@@ -101,6 +117,17 @@ module Kward
|
|
|
101
117
|
File.join(cache_dir, "openrouter_models.json")
|
|
102
118
|
end
|
|
103
119
|
|
|
120
|
+
def project_browser_state_path
|
|
121
|
+
File.join(cache_dir, "project_browser_state.json")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def prompt_history_path(cwd, config_dir: self.config_dir, kind: "prompt")
|
|
125
|
+
key = Digest::SHA256.hexdigest(canonical_workspace_root(cwd))[0, 24]
|
|
126
|
+
return File.join(config_dir, "history", "#{key}.jsonl") if kind.to_s == "prompt"
|
|
127
|
+
|
|
128
|
+
File.join(config_dir, "history", kind.to_s, "#{key}.jsonl")
|
|
129
|
+
end
|
|
130
|
+
|
|
104
131
|
# @return [String] directory containing structured memory files
|
|
105
132
|
def memory_dir
|
|
106
133
|
File.join(config_dir, "memory")
|
|
@@ -144,6 +171,69 @@ module Kward
|
|
|
144
171
|
PrivateFile.write_json(path, config)
|
|
145
172
|
end
|
|
146
173
|
|
|
174
|
+
def read_ekwsh_config(path = ekwsh_config_path)
|
|
175
|
+
path = File.expand_path(path)
|
|
176
|
+
return normalize_ekwsh_config(nil) unless File.exist?(path)
|
|
177
|
+
|
|
178
|
+
data = YAML.safe_load(File.read(path), permitted_classes: [], aliases: false)
|
|
179
|
+
normalize_ekwsh_config(data)
|
|
180
|
+
rescue Psych::SyntaxError => e
|
|
181
|
+
raise "Invalid ekwsh YAML config: #{path}: #{e.message}"
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def normalize_ekwsh_config(data)
|
|
185
|
+
data = data.transform_keys(&:to_s) if data.is_a?(Hash)
|
|
186
|
+
settings = data.is_a?(Hash) ? data : {}
|
|
187
|
+
{
|
|
188
|
+
shell: normalize_ekwsh_shell(settings["shell"]),
|
|
189
|
+
timeout_seconds: normalize_positive_integer(settings["timeout_seconds"], Ekwsh::DEFAULT_TIMEOUT_SECONDS),
|
|
190
|
+
max_output_bytes: normalize_positive_integer(settings["max_output_bytes"], Ekwsh::DEFAULT_MAX_OUTPUT_BYTES),
|
|
191
|
+
history_limit: normalize_positive_integer(settings["history_limit"], Ekwsh::DEFAULT_HISTORY_LIMIT),
|
|
192
|
+
env: normalize_ekwsh_env(settings["env"]),
|
|
193
|
+
aliases: normalize_ekwsh_aliases(settings["aliases"])
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def normalize_ekwsh_shell(value)
|
|
198
|
+
shell = value.to_s.strip
|
|
199
|
+
return Ekwsh::DEFAULT_SHELL if shell.empty?
|
|
200
|
+
return shell if shell.start_with?("/") && File.executable?(shell)
|
|
201
|
+
|
|
202
|
+
Ekwsh::DEFAULT_SHELL
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def normalize_positive_integer(value, default)
|
|
206
|
+
integer = Integer(value)
|
|
207
|
+
integer.positive? ? integer : default
|
|
208
|
+
rescue ArgumentError, TypeError
|
|
209
|
+
default
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def normalize_ekwsh_env(values)
|
|
213
|
+
return {} unless values.is_a?(Hash)
|
|
214
|
+
|
|
215
|
+
values.each_with_object({}) do |(key, value), result|
|
|
216
|
+
key = key.to_s
|
|
217
|
+
next unless key.match?(/\A[A-Za-z_][A-Za-z0-9_]*\z/)
|
|
218
|
+
next if value.nil?
|
|
219
|
+
|
|
220
|
+
result[key] = value.to_s
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def normalize_ekwsh_aliases(values)
|
|
225
|
+
return {} unless values.is_a?(Hash)
|
|
226
|
+
|
|
227
|
+
values.each_with_object({}) do |(name, command), result|
|
|
228
|
+
name = name.to_s
|
|
229
|
+
command = command.to_s.strip
|
|
230
|
+
next unless Ekwsh.valid_alias_name?(name)
|
|
231
|
+
next if command.empty?
|
|
232
|
+
|
|
233
|
+
result[name] = command
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
147
237
|
# Merges top-level config values and writes the updated config privately.
|
|
148
238
|
def update_config(values, path = config_path)
|
|
149
239
|
raise "Config values must be an object" unless values.is_a?(Hash)
|
|
@@ -154,6 +244,17 @@ module Kward
|
|
|
154
244
|
config
|
|
155
245
|
end
|
|
156
246
|
|
|
247
|
+
# Merges values into a one-level nested config section and writes privately.
|
|
248
|
+
def update_nested_config(section, values, path = config_path)
|
|
249
|
+
raise "Config values must be an object" unless values.is_a?(Hash)
|
|
250
|
+
|
|
251
|
+
config = read_config(path)
|
|
252
|
+
current = config[section.to_s].is_a?(Hash) ? config[section.to_s].dup : {}
|
|
253
|
+
config[section.to_s] = current.merge(values.transform_keys(&:to_s))
|
|
254
|
+
write_config(config, path)
|
|
255
|
+
config
|
|
256
|
+
end
|
|
257
|
+
|
|
157
258
|
# Removes a top-level config key when it exists.
|
|
158
259
|
def delete_config_key(key, path = config_path)
|
|
159
260
|
config = read_config(path)
|
|
@@ -193,6 +294,49 @@ module Kward
|
|
|
193
294
|
composer["busy_help"] != false
|
|
194
295
|
end
|
|
195
296
|
|
|
297
|
+
# Returns the configured tab keybinding family, or auto when unset/invalid.
|
|
298
|
+
def composer_tab_keybindings(config = read_config)
|
|
299
|
+
composer = config["composer"].is_a?(Hash) ? config["composer"] : {}
|
|
300
|
+
value = composer["tab_keybindings"].to_s.downcase
|
|
301
|
+
%w[auto ctrl alt].include?(value) ? value : "auto"
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Returns the built-in TUI editor keymap mode.
|
|
305
|
+
def editor_mode(config = read_config)
|
|
306
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
307
|
+
EditorMode.normalize(editor["mode"])
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
# Returns whether the built-in TUI editor should auto-indent new lines.
|
|
311
|
+
def editor_auto_indent?(config = read_config)
|
|
312
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
313
|
+
editor["auto_indent"] != false
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Returns whether the built-in TUI editor should auto-close typed pairs.
|
|
317
|
+
def editor_auto_close_pairs?(config = read_config)
|
|
318
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
319
|
+
editor["auto_close_pairs"] != false
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Returns whether the built-in TUI editor should soft-wrap long lines.
|
|
323
|
+
def editor_soft_wrap?(config = read_config)
|
|
324
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
325
|
+
editor["soft_wrap"] != false
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Returns whether editable built-in TUI editor buffers should use a bar cursor.
|
|
329
|
+
def editor_bar_cursor?(config = read_config)
|
|
330
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
331
|
+
editor["bar_cursor"] != false
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Returns the built-in TUI editor line-number display mode.
|
|
335
|
+
def editor_line_numbers(config = read_config)
|
|
336
|
+
editor = config["editor"].is_a?(Hash) ? config["editor"] : {}
|
|
337
|
+
EditorMode.normalize_line_numbers(editor["line_numbers"])
|
|
338
|
+
end
|
|
339
|
+
|
|
196
340
|
# Returns whether file tools must stay inside the active workspace root.
|
|
197
341
|
def workspace_guardrails_enabled?(config = read_config)
|
|
198
342
|
tools = config["tools"].is_a?(Hash) ? config["tools"] : {}
|
|
@@ -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
|