kward 0.66.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -3
- data/Gemfile.lock +2 -2
- data/README.md +5 -1
- data/doc/configuration.md +43 -1
- data/doc/memory.md +31 -9
- data/doc/rpc.md +41 -21
- data/doc/troubleshooting.md +55 -0
- data/doc/usage.md +41 -6
- data/lib/kward/cli.rb +1155 -195
- data/lib/kward/cli_transcript_formatter.rb +124 -0
- data/lib/kward/compaction/file_operation_tracker.rb +46 -0
- data/lib/kward/compactor.rb +3 -68
- data/lib/kward/config_files.rb +45 -69
- data/lib/kward/memory/manager.rb +66 -7
- data/lib/kward/model/client.rb +2 -195
- data/lib/kward/model/model_info.rb +9 -10
- data/lib/kward/model/payloads.rb +203 -0
- data/lib/kward/prompt_interface/banner.rb +77 -0
- data/lib/kward/prompt_interface.rb +220 -191
- data/lib/kward/prompts/commands.rb +3 -2
- data/lib/kward/rpc/runtime_payloads.rb +79 -0
- data/lib/kward/rpc/server.rb +33 -34
- data/lib/kward/rpc/session_manager.rb +518 -159
- data/lib/kward/rpc/tool_event_normalizer.rb +12 -9
- data/lib/kward/rpc/transcript_normalizer.rb +31 -53
- data/lib/kward/session_store.rb +262 -20
- data/lib/kward/session_trash.rb +96 -0
- data/lib/kward/session_tree_renderer.rb +264 -0
- data/lib/kward/tools/registry.rb +3 -1
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workspace.rb +10 -5
- metadata +9 -1
data/lib/kward/cli.rb
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
require "
|
|
1
|
+
require "fileutils"
|
|
2
2
|
require "json"
|
|
3
3
|
require "thread"
|
|
4
4
|
require "tty-prompt"
|
|
5
5
|
require_relative "agent"
|
|
6
6
|
require_relative "ansi"
|
|
7
|
+
require_relative "version"
|
|
7
8
|
require_relative "model/client"
|
|
8
9
|
require_relative "compactor"
|
|
9
10
|
require_relative "config_files"
|
|
10
11
|
require_relative "clipboard"
|
|
12
|
+
require_relative "cli_transcript_formatter"
|
|
11
13
|
require_relative "model/context_usage"
|
|
12
14
|
require_relative "events"
|
|
13
15
|
require_relative "export_path"
|
|
@@ -26,6 +28,7 @@ require_relative "model/retry_message"
|
|
|
26
28
|
require_relative "rpc/server"
|
|
27
29
|
require_relative "session_diff"
|
|
28
30
|
require_relative "session_store"
|
|
31
|
+
require_relative "session_tree_renderer"
|
|
29
32
|
require_relative "starter_pack_installer"
|
|
30
33
|
require_relative "steering"
|
|
31
34
|
require_relative "tools/tool_call"
|
|
@@ -57,6 +60,8 @@ module Kward
|
|
|
57
60
|
@session_diff = SessionDiff.new
|
|
58
61
|
@cleanup_sessions = []
|
|
59
62
|
@plugin_registry = nil
|
|
63
|
+
@working_directory = nil
|
|
64
|
+
@prompt_delimited = false
|
|
60
65
|
@color_enabled = ANSI.enabled?($stdout)
|
|
61
66
|
end
|
|
62
67
|
|
|
@@ -65,35 +70,117 @@ module Kward
|
|
|
65
70
|
#
|
|
66
71
|
# @return [void]
|
|
67
72
|
def run
|
|
73
|
+
@argv = extract_global_options(@argv)
|
|
74
|
+
with_working_directory { dispatch }
|
|
75
|
+
rescue ArgumentError => e
|
|
76
|
+
warn e.message
|
|
77
|
+
warn "Run `kward help` for available commands."
|
|
78
|
+
exit 1
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def dispatch
|
|
82
|
+
if @prompt_delimited
|
|
83
|
+
ConfigFiles.ensure_default_config!
|
|
84
|
+
run_prompt_or_interactive
|
|
85
|
+
return
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if help_command?
|
|
89
|
+
print_command_help(@argv[1])
|
|
90
|
+
return
|
|
91
|
+
end
|
|
92
|
+
raise ArgumentError, command_usage("help") if ["help", "--help", "-h"].include?(@argv.first)
|
|
93
|
+
|
|
94
|
+
if version_command?
|
|
95
|
+
print_version
|
|
96
|
+
return
|
|
97
|
+
end
|
|
98
|
+
raise ArgumentError, command_usage("version") if ["version", "--version", "-v"].include?(@argv.first)
|
|
99
|
+
|
|
68
100
|
ConfigFiles.ensure_default_config!
|
|
69
101
|
|
|
102
|
+
if @argv.first == "init"
|
|
103
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
104
|
+
print_command_help("init")
|
|
105
|
+
return
|
|
106
|
+
end
|
|
107
|
+
raise ArgumentError, command_usage("init") unless @argv.length == 1
|
|
108
|
+
|
|
109
|
+
install_starter_pack
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
|
|
70
113
|
if @argv == ["--install-starter-pack"]
|
|
71
114
|
install_starter_pack
|
|
72
115
|
return
|
|
73
116
|
end
|
|
74
117
|
|
|
75
|
-
if @argv.first == "
|
|
118
|
+
if @argv.first == "auth"
|
|
119
|
+
handle_auth_command(@argv[1..] || [])
|
|
120
|
+
return
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if @argv.first == "doctor"
|
|
124
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
125
|
+
print_command_help("doctor")
|
|
126
|
+
return
|
|
127
|
+
end
|
|
128
|
+
raise ArgumentError, command_usage("doctor") unless @argv.length == 1
|
|
129
|
+
|
|
130
|
+
print_doctor
|
|
131
|
+
return
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
if @argv.first == "rpc"
|
|
135
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
136
|
+
print_command_help("rpc")
|
|
137
|
+
return
|
|
138
|
+
end
|
|
139
|
+
raise ArgumentError, command_usage("rpc") unless @argv.length == 1
|
|
140
|
+
|
|
76
141
|
Kward::RPC::Server.new(input: @stdin, output: $stdout, client: @client).run
|
|
77
142
|
return
|
|
78
143
|
end
|
|
79
144
|
|
|
80
|
-
if @argv
|
|
145
|
+
if @argv.first == "stats"
|
|
146
|
+
if @argv[1] == "tokens" && help_option_arguments?(@argv[2..] || [])
|
|
147
|
+
print_command_help("stats")
|
|
148
|
+
return
|
|
149
|
+
end
|
|
150
|
+
raise ArgumentError, command_usage("stats") unless @argv[1] == "tokens"
|
|
151
|
+
|
|
81
152
|
export_token_stats(@argv[2..] || [])
|
|
82
153
|
return
|
|
83
154
|
end
|
|
84
155
|
|
|
85
156
|
if pan_mode?
|
|
86
|
-
|
|
157
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
158
|
+
print_command_help("pan")
|
|
159
|
+
return
|
|
160
|
+
end
|
|
161
|
+
raise ArgumentError, command_usage("pan") unless @argv.length == 1
|
|
162
|
+
|
|
163
|
+
PanServer.new(client: @client, working_directory: current_workspace_root).run
|
|
87
164
|
return
|
|
88
165
|
end
|
|
89
166
|
|
|
90
|
-
if ["login", "--login"].include?(@argv.first)
|
|
167
|
+
if ["login", "--login"].include?(@argv.first)
|
|
168
|
+
if help_option_arguments?(@argv[1..] || [])
|
|
169
|
+
print_command_help("login")
|
|
170
|
+
return
|
|
171
|
+
end
|
|
172
|
+
raise ArgumentError, command_usage("login") unless @argv.length <= 2
|
|
173
|
+
|
|
91
174
|
login(provider: @argv[1])
|
|
92
175
|
return
|
|
93
176
|
end
|
|
94
177
|
|
|
95
|
-
|
|
96
|
-
|
|
178
|
+
run_prompt_or_interactive
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def run_prompt_or_interactive
|
|
182
|
+
first_prompt = one_shot_prompt_argument
|
|
183
|
+
if first_prompt
|
|
97
184
|
answer = one_shot(first_prompt)
|
|
98
185
|
puts answer unless answer.empty?
|
|
99
186
|
return
|
|
@@ -116,7 +203,7 @@ module Kward
|
|
|
116
203
|
conversation = new_conversation
|
|
117
204
|
agent = Agent.new(
|
|
118
205
|
client: @client,
|
|
119
|
-
tool_registry: ToolRegistry.new(prompt: @prompt),
|
|
206
|
+
tool_registry: ToolRegistry.new(workspace: configured_workspace, prompt: @prompt),
|
|
120
207
|
conversation: conversation
|
|
121
208
|
)
|
|
122
209
|
answer = agent.ask(input) do |event|
|
|
@@ -146,6 +233,54 @@ module Kward
|
|
|
146
233
|
assistant_streamed ? "" : render_markdown_transcript(answer)
|
|
147
234
|
end
|
|
148
235
|
|
|
236
|
+
def handle_auth_command(arguments)
|
|
237
|
+
if help_option_arguments?(arguments)
|
|
238
|
+
print_command_help("auth")
|
|
239
|
+
return
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
case arguments
|
|
243
|
+
when ["status"]
|
|
244
|
+
print_auth_status
|
|
245
|
+
when ["logout"]
|
|
246
|
+
logout_auth
|
|
247
|
+
else
|
|
248
|
+
raise ArgumentError, command_usage("auth")
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def print_auth_status
|
|
253
|
+
config = safely_read_config.to_h
|
|
254
|
+
lines = ["#{colored("Auth Status", :green, :bold)}", ""]
|
|
255
|
+
lines << auth_status_line("OpenAI OAuth", File.exist?(OpenAIOAuth.default_auth_path), OpenAIOAuth.default_auth_path)
|
|
256
|
+
lines << auth_status_line("GitHub OAuth", File.exist?(GithubOAuth.default_auth_path), GithubOAuth.default_auth_path)
|
|
257
|
+
lines << auth_status_line("OpenRouter API key", !config["openrouter_api_key"].to_s.empty? || !ENV["OPENROUTER_API_KEY"].to_s.empty?, ConfigFiles.config_path)
|
|
258
|
+
@prompt.say lines.join("\n")
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def auth_status_line(label, configured, location)
|
|
262
|
+
status = configured ? :ok : :warning
|
|
263
|
+
message = configured ? "configured" : "not configured"
|
|
264
|
+
"#{doctor_mark(status)} #{label}: #{message} (#{location})"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def logout_auth
|
|
268
|
+
removed = []
|
|
269
|
+
[OpenAIOAuth.default_auth_path, GithubOAuth.default_auth_path].each do |path|
|
|
270
|
+
next unless File.exist?(path)
|
|
271
|
+
|
|
272
|
+
File.delete(path)
|
|
273
|
+
removed << path
|
|
274
|
+
end
|
|
275
|
+
removed << "OpenRouter API key" if OpenRouterAPIKey.new.logout
|
|
276
|
+
|
|
277
|
+
if removed.empty?
|
|
278
|
+
@prompt.say "No saved credentials found."
|
|
279
|
+
else
|
|
280
|
+
@prompt.say "Removed #{removed.length} saved credential#{removed.length == 1 ? "" : "s"}."
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
149
284
|
def login(provider: nil, oauth: nil)
|
|
150
285
|
provider = provider.to_s.downcase
|
|
151
286
|
if provider == "openrouter"
|
|
@@ -164,12 +299,9 @@ module Kward
|
|
|
164
299
|
def interactive_loop(agent: nil)
|
|
165
300
|
setup_interactive_prompt
|
|
166
301
|
session_store = interactive_session_store(agent)
|
|
302
|
+
@resumed_last_session = false
|
|
167
303
|
if session_store && agent.nil?
|
|
168
|
-
|
|
169
|
-
reset_session_diff
|
|
170
|
-
conversation = new_conversation(workspace_root: session_store.cwd)
|
|
171
|
-
@active_session.attach(conversation)
|
|
172
|
-
agent = build_interactive_agent(conversation)
|
|
304
|
+
agent = resume_last_session(session_store) || build_new_session_agent(session_store)
|
|
173
305
|
elsif session_store
|
|
174
306
|
@active_session = track_session(session_store.create(model: current_model_id, reasoning_effort: current_reasoning_effort))
|
|
175
307
|
reset_session_diff
|
|
@@ -181,7 +313,8 @@ module Kward
|
|
|
181
313
|
update_assistant_prompt(agent.conversation)
|
|
182
314
|
@footer_conversation = agent.conversation
|
|
183
315
|
|
|
184
|
-
print_visual_banner
|
|
316
|
+
print_visual_banner unless @resumed_last_session
|
|
317
|
+
render_resumed_last_session_transcript(agent.conversation) if @resumed_last_session
|
|
185
318
|
|
|
186
319
|
@pending_inputs = []
|
|
187
320
|
|
|
@@ -207,12 +340,14 @@ module Kward
|
|
|
207
340
|
agent = replacement_agent if replacement_agent
|
|
208
341
|
end
|
|
209
342
|
next if handled
|
|
343
|
+
next if shell_command_input?(command_input) && handle_interactive_shell_command(command_input, agent)
|
|
210
344
|
|
|
211
345
|
expanded_input = expand_prompt_template(input)
|
|
212
346
|
display_input = display_input || input if expanded_input
|
|
213
347
|
input = expanded_input || input
|
|
214
348
|
@footer_conversation = agent.conversation
|
|
215
349
|
begin
|
|
350
|
+
auto_name_active_session(display_input || input)
|
|
216
351
|
pending_inputs = run_interactive_turn(agent, input, display_input: display_input)
|
|
217
352
|
pending_inputs.reverse_each { |pending_input| @pending_inputs.unshift(pending_input) }
|
|
218
353
|
rescue StandardError => e
|
|
@@ -229,6 +364,7 @@ module Kward
|
|
|
229
364
|
@prompt.close if prompt_interface?
|
|
230
365
|
ensure
|
|
231
366
|
cleanup_unused_sessions
|
|
367
|
+
remember_active_session(session_store)
|
|
232
368
|
end
|
|
233
369
|
end
|
|
234
370
|
|
|
@@ -240,6 +376,266 @@ module Kward
|
|
|
240
376
|
|
|
241
377
|
private
|
|
242
378
|
|
|
379
|
+
def help_command?
|
|
380
|
+
["help", "--help", "-h"].include?(@argv.first) && @argv.length <= 2
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
def version_command?
|
|
384
|
+
["version", "--version", "-v"].include?(@argv.first) && @argv.length == 1
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def help_option_arguments?(arguments)
|
|
388
|
+
arguments.length == 1 && ["help", "--help", "-h"].include?(arguments.first)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def one_shot_prompt_argument
|
|
392
|
+
prompt = @argv.join(" ").strip
|
|
393
|
+
prompt.empty? ? nil : prompt
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def print_command_help(command_name = nil)
|
|
397
|
+
if command_name.to_s.empty? || ["--help", "-h"].include?(command_name)
|
|
398
|
+
print_help
|
|
399
|
+
return
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
help = command_help[command_name]
|
|
403
|
+
raise ArgumentError, "Unknown command: #{command_name}" unless help
|
|
404
|
+
|
|
405
|
+
@prompt.say render_command_help(command_name, help)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def print_help
|
|
409
|
+
command = ->(text) { colored(text, :green, :bold) }
|
|
410
|
+
option = ->(text) { colored(text, :cyan) }
|
|
411
|
+
heading = ->(text) { colored(text, :blue, :bold) }
|
|
412
|
+
|
|
413
|
+
@prompt.say <<~HELP.rstrip
|
|
414
|
+
#{colored("Kward", :green, :bold)} - an extendable CLI coding agent
|
|
415
|
+
|
|
416
|
+
#{heading.call("Usage")}
|
|
417
|
+
#{command.call("kward")} Start an interactive chat
|
|
418
|
+
#{command.call("kward")} #{option.call('"Explain this project"')} Run a one-shot prompt
|
|
419
|
+
#{command.call("kward login")} Sign in or save provider credentials
|
|
420
|
+
#{command.call("kward auth status")} Show saved credential status
|
|
421
|
+
#{command.call("kward init")} Install starter prompts and AGENTS.md
|
|
422
|
+
#{command.call("kward doctor")} Check local Kward setup
|
|
423
|
+
#{command.call("kward pan")} Start Pan mode web UI
|
|
424
|
+
#{command.call("kward rpc")} Start the experimental JSON-RPC backend
|
|
425
|
+
|
|
426
|
+
#{heading.call("Commands")}
|
|
427
|
+
#{command.call("help")} Show this help
|
|
428
|
+
#{command.call("version")} Show the installed Kward version
|
|
429
|
+
#{command.call("login")} [openrouter|github] Sign in with OpenAI, OpenRouter, or GitHub
|
|
430
|
+
#{command.call("auth status|logout")} Show or clear saved credentials
|
|
431
|
+
#{command.call("init")} Install starter prompts and AGENTS.md
|
|
432
|
+
#{command.call("doctor")} Check local Kward setup
|
|
433
|
+
#{command.call("stats tokens")} [range] [options] Export local token telemetry as CSV
|
|
434
|
+
#{command.call("pan")} Start Pan mode web UI
|
|
435
|
+
#{command.call("rpc")} Run the JSON-RPC backend for UI clients
|
|
436
|
+
|
|
437
|
+
#{heading.call("Options")}
|
|
438
|
+
#{option.call("--working-directory=PATH")} Run Kward from PATH
|
|
439
|
+
#{option.call("--help")}, #{option.call("-h")} Show this help
|
|
440
|
+
#{option.call("--version")}, #{option.call("-v")} Show the installed version
|
|
441
|
+
|
|
442
|
+
#{heading.call("Examples")}
|
|
443
|
+
#{command.call("kward")}
|
|
444
|
+
#{command.call("kward")} #{option.call('"Review this diff"')}
|
|
445
|
+
#{command.call("git diff | kward")} #{option.call('"Review this diff"')}
|
|
446
|
+
#{command.call("kward login openrouter")}
|
|
447
|
+
#{command.call("kward stats tokens today --bucket hour")}
|
|
448
|
+
|
|
449
|
+
Command names take precedence. Anything else is sent as a one-shot prompt.
|
|
450
|
+
HELP
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
def command_help
|
|
454
|
+
{
|
|
455
|
+
"help" => {
|
|
456
|
+
usage: "kward help [command]",
|
|
457
|
+
description: "Show the top-level command overview or help for one command.",
|
|
458
|
+
examples: ["kward help", "kward help pan"]
|
|
459
|
+
},
|
|
460
|
+
"version" => {
|
|
461
|
+
usage: "kward version",
|
|
462
|
+
description: "Show the installed Kward version.",
|
|
463
|
+
examples: ["kward version", "kward --version"]
|
|
464
|
+
},
|
|
465
|
+
"login" => {
|
|
466
|
+
usage: "kward login [openrouter|github]",
|
|
467
|
+
description: "Sign in with OpenAI, OpenRouter, or GitHub.",
|
|
468
|
+
examples: ["kward login", "kward login openrouter", "kward login github"]
|
|
469
|
+
},
|
|
470
|
+
"auth" => {
|
|
471
|
+
usage: "kward auth status|logout",
|
|
472
|
+
description: "Show or clear saved provider credentials without printing secrets.",
|
|
473
|
+
examples: ["kward auth status", "kward auth logout"]
|
|
474
|
+
},
|
|
475
|
+
"init" => {
|
|
476
|
+
usage: "kward init",
|
|
477
|
+
description: "Install starter prompts and base AGENTS.md into your config directory.",
|
|
478
|
+
examples: ["kward init"]
|
|
479
|
+
},
|
|
480
|
+
"doctor" => {
|
|
481
|
+
usage: "kward doctor",
|
|
482
|
+
description: "Check local Kward configuration, workspace, auth hints, and writable directories.",
|
|
483
|
+
examples: ["kward doctor", "kward --working-directory ~/code/project doctor"]
|
|
484
|
+
},
|
|
485
|
+
"stats" => {
|
|
486
|
+
usage: "kward stats tokens [range] [--bucket second|minute|hour|day|week|month|year] [--output path]",
|
|
487
|
+
description: "Export local token telemetry as CSV.",
|
|
488
|
+
examples: ["kward stats tokens today", "kward stats tokens today --bucket hour", "kward stats tokens week --output tokens.csv"]
|
|
489
|
+
},
|
|
490
|
+
"pan" => {
|
|
491
|
+
usage: "kward pan",
|
|
492
|
+
description: "Start Pan mode, a minimal LAN web UI with a prompt textarea and transcript.",
|
|
493
|
+
examples: ["kward pan", "kward --working-directory ~/code/project pan"]
|
|
494
|
+
},
|
|
495
|
+
"rpc" => {
|
|
496
|
+
usage: "kward rpc",
|
|
497
|
+
description: "Start the experimental JSON-RPC backend for UI clients.",
|
|
498
|
+
examples: ["kward rpc", "kward --working-directory ~/code/project rpc"]
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
def render_command_help(name, help)
|
|
504
|
+
heading = ->(text) { colored(text, :blue, :bold) }
|
|
505
|
+
command = ->(text) { colored(text, :green, :bold) }
|
|
506
|
+
|
|
507
|
+
lines = [
|
|
508
|
+
"#{command.call(name)} - #{help.fetch(:description)}",
|
|
509
|
+
"",
|
|
510
|
+
heading.call("Usage"),
|
|
511
|
+
" #{command.call(help.fetch(:usage))}"
|
|
512
|
+
]
|
|
513
|
+
examples = help.fetch(:examples, [])
|
|
514
|
+
if examples.any?
|
|
515
|
+
lines << ""
|
|
516
|
+
lines << heading.call("Examples")
|
|
517
|
+
examples.each { |example| lines << " #{command.call(example)}" }
|
|
518
|
+
end
|
|
519
|
+
lines.join("\n")
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def command_usage(name)
|
|
523
|
+
"Usage: #{command_help.fetch(name).fetch(:usage)}"
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def print_version
|
|
527
|
+
@prompt.say "kward #{VERSION}"
|
|
528
|
+
end
|
|
529
|
+
|
|
530
|
+
def print_doctor
|
|
531
|
+
lines = ["#{colored("Kward Doctor", :green, :bold)}", ""]
|
|
532
|
+
doctor_checks.each do |check|
|
|
533
|
+
lines << "#{doctor_mark(check.fetch(:status))} #{check.fetch(:label)}: #{check.fetch(:message)}"
|
|
534
|
+
end
|
|
535
|
+
@prompt.say lines.join("\n")
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def doctor_checks
|
|
539
|
+
config = safely_read_config
|
|
540
|
+
[
|
|
541
|
+
doctor_config_check,
|
|
542
|
+
doctor_config_json_check(config),
|
|
543
|
+
doctor_directory_check("Config directory", ConfigFiles.config_dir),
|
|
544
|
+
doctor_directory_check("Session directory", SessionStore.new(cwd: current_workspace_root).session_dir, create: true),
|
|
545
|
+
doctor_workspace_check,
|
|
546
|
+
doctor_model_check,
|
|
547
|
+
doctor_auth_check(config),
|
|
548
|
+
doctor_pan_check(config),
|
|
549
|
+
{ status: :ok, label: "Color", message: @color_enabled ? "enabled" : "disabled" }
|
|
550
|
+
]
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def safely_read_config
|
|
554
|
+
ConfigFiles.read_config
|
|
555
|
+
rescue StandardError
|
|
556
|
+
nil
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
def doctor_config_check
|
|
560
|
+
path = ConfigFiles.config_path
|
|
561
|
+
if File.exist?(path)
|
|
562
|
+
readable = File.readable?(path)
|
|
563
|
+
return { status: readable ? :ok : :error, label: "Config", message: readable ? path : "not readable: #{path}" }
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
{ status: :warning, label: "Config", message: "not found: #{path}" }
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def doctor_config_json_check(config)
|
|
570
|
+
return { status: :error, label: "Config JSON", message: "invalid or unreadable" } unless config.is_a?(Hash)
|
|
571
|
+
|
|
572
|
+
{ status: :ok, label: "Config JSON", message: "valid" }
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
def doctor_directory_check(label, path, create: false)
|
|
576
|
+
FileUtils.mkdir_p(path, mode: 0o700) if create
|
|
577
|
+
if Dir.exist?(path) && File.writable?(path)
|
|
578
|
+
{ status: :ok, label: label, message: "writable: #{path}" }
|
|
579
|
+
elsif Dir.exist?(path)
|
|
580
|
+
{ status: :error, label: label, message: "not writable: #{path}" }
|
|
581
|
+
else
|
|
582
|
+
{ status: :error, label: label, message: "missing: #{path}" }
|
|
583
|
+
end
|
|
584
|
+
rescue StandardError => e
|
|
585
|
+
{ status: :error, label: label, message: e.message }
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def doctor_workspace_check
|
|
589
|
+
root = current_workspace_root
|
|
590
|
+
return { status: :ok, label: "Workspace", message: root } if Dir.exist?(root) && File.directory?(root)
|
|
591
|
+
|
|
592
|
+
{ status: :error, label: "Workspace", message: "not a directory: #{root}" }
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def doctor_model_check
|
|
596
|
+
provider = @client.current_provider if @client.respond_to?(:current_provider)
|
|
597
|
+
model = @client.current_model if @client.respond_to?(:current_model)
|
|
598
|
+
parts = [provider, model].compact.map(&:to_s).reject(&:empty?)
|
|
599
|
+
return { status: :ok, label: "Model", message: parts.join(" / ") } if parts.any?
|
|
600
|
+
|
|
601
|
+
{ status: :warning, label: "Model", message: "not configured" }
|
|
602
|
+
rescue StandardError => e
|
|
603
|
+
{ status: :warning, label: "Model", message: e.message }
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def doctor_auth_check(config)
|
|
607
|
+
openai_auth = OpenAIOAuth.default_auth_path
|
|
608
|
+
github_auth = GithubOAuth.default_auth_path
|
|
609
|
+
has_openrouter = !config.to_h["openrouter_api_key"].to_s.empty? || !ENV["OPENROUTER_API_KEY"].to_s.empty?
|
|
610
|
+
paths = []
|
|
611
|
+
paths << "OpenAI OAuth" if File.exist?(openai_auth)
|
|
612
|
+
paths << "GitHub OAuth" if File.exist?(github_auth)
|
|
613
|
+
paths << "OpenRouter API key" if has_openrouter
|
|
614
|
+
return { status: :ok, label: "Auth", message: paths.join(", ") } if paths.any?
|
|
615
|
+
|
|
616
|
+
{ status: :warning, label: "Auth", message: "no saved credentials found; run `kward login`" }
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def doctor_pan_check(config)
|
|
620
|
+
pan = config.to_h["pan_mode"] || config.to_h["panMode"] || {}
|
|
621
|
+
if !pan["username"].to_s.empty? && !pan["password"].to_s.empty?
|
|
622
|
+
{ status: :ok, label: "Pan mode", message: "credentials configured" }
|
|
623
|
+
else
|
|
624
|
+
{ status: :warning, label: "Pan mode", message: "username/password not configured" }
|
|
625
|
+
end
|
|
626
|
+
end
|
|
627
|
+
|
|
628
|
+
def doctor_mark(status)
|
|
629
|
+
case status
|
|
630
|
+
when :ok
|
|
631
|
+
colored("✓", :green, :bold)
|
|
632
|
+
when :warning
|
|
633
|
+
colored("!", :yellow, :bold)
|
|
634
|
+
else
|
|
635
|
+
colored("✗", :red, :bold)
|
|
636
|
+
end
|
|
637
|
+
end
|
|
638
|
+
|
|
243
639
|
def install_starter_pack
|
|
244
640
|
result = StarterPackInstaller.install
|
|
245
641
|
installed_count = result.installed.length
|
|
@@ -252,7 +648,7 @@ module Kward
|
|
|
252
648
|
end
|
|
253
649
|
|
|
254
650
|
def pan_mode?
|
|
255
|
-
|
|
651
|
+
["pan", "--pan-mode"].include?(@argv.first)
|
|
256
652
|
end
|
|
257
653
|
|
|
258
654
|
def export_token_stats(arguments)
|
|
@@ -299,17 +695,46 @@ module Kward
|
|
|
299
695
|
{ range: remaining.join(" "), bucket: bucket, output: output }
|
|
300
696
|
end
|
|
301
697
|
|
|
302
|
-
def
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
698
|
+
def extract_global_options(arguments)
|
|
699
|
+
remaining = []
|
|
700
|
+
index = 0
|
|
701
|
+
while index < arguments.length
|
|
702
|
+
argument = arguments[index]
|
|
703
|
+
case argument
|
|
704
|
+
when "--"
|
|
705
|
+
@prompt_delimited = true
|
|
706
|
+
remaining.concat(arguments[(index + 1)..] || [])
|
|
707
|
+
break
|
|
708
|
+
when "--working-directory"
|
|
709
|
+
index += 1
|
|
710
|
+
raise ArgumentError, "Missing value for --working-directory" if index >= arguments.length
|
|
306
711
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
712
|
+
@working_directory = expanded_working_directory(arguments[index])
|
|
713
|
+
when /\A--working-directory=(.*)\z/
|
|
714
|
+
@working_directory = expanded_working_directory(Regexp.last_match(1))
|
|
715
|
+
else
|
|
716
|
+
remaining << argument
|
|
717
|
+
end
|
|
718
|
+
index += 1
|
|
311
719
|
end
|
|
312
|
-
|
|
720
|
+
remaining
|
|
721
|
+
end
|
|
722
|
+
|
|
723
|
+
def expanded_working_directory(path)
|
|
724
|
+
value = path.to_s.strip
|
|
725
|
+
raise ArgumentError, "Missing value for --working-directory" if value.empty?
|
|
726
|
+
|
|
727
|
+
expanded = File.expand_path(value)
|
|
728
|
+
raise ArgumentError, "Working directory does not exist: #{expanded}" unless Dir.exist?(expanded)
|
|
729
|
+
raise ArgumentError, "Working directory is not a directory: #{expanded}" unless File.directory?(expanded)
|
|
730
|
+
|
|
731
|
+
expanded
|
|
732
|
+
end
|
|
733
|
+
|
|
734
|
+
def with_working_directory
|
|
735
|
+
return yield unless @working_directory
|
|
736
|
+
|
|
737
|
+
Dir.chdir(@working_directory) { yield }
|
|
313
738
|
end
|
|
314
739
|
|
|
315
740
|
def interactive_session_store(agent)
|
|
@@ -319,6 +744,43 @@ module Kward
|
|
|
319
744
|
SessionStore.new
|
|
320
745
|
end
|
|
321
746
|
|
|
747
|
+
def resume_last_session(session_store)
|
|
748
|
+
return nil unless session_auto_resume_enabled?
|
|
749
|
+
|
|
750
|
+
path = session_store.remembered_last_session_path if session_store.respond_to?(:remembered_last_session_path)
|
|
751
|
+
return nil if path.to_s.empty?
|
|
752
|
+
|
|
753
|
+
@active_session, conversation = session_store.load(path, workspace: configured_workspace(root: session_store.cwd), model: current_model_id, reasoning_effort: current_reasoning_effort)
|
|
754
|
+
reset_session_diff(@active_session.path)
|
|
755
|
+
track_session(@active_session)
|
|
756
|
+
@resumed_last_session = true
|
|
757
|
+
build_interactive_agent(conversation)
|
|
758
|
+
rescue StandardError
|
|
759
|
+
nil
|
|
760
|
+
end
|
|
761
|
+
|
|
762
|
+
def render_resumed_last_session_transcript(conversation)
|
|
763
|
+
restore_prompt_transcript do
|
|
764
|
+
@prompt.say("\nResumed session: #{@active_session.path}\n")
|
|
765
|
+
render_conversation_transcript(conversation)
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
def remember_active_session(session_store)
|
|
770
|
+
return unless session_store&.respond_to?(:remember_last_session)
|
|
771
|
+
return unless @active_session&.path && File.file?(@active_session.path)
|
|
772
|
+
|
|
773
|
+
session_store.remember_last_session(@active_session)
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def build_new_session_agent(session_store)
|
|
777
|
+
@active_session = track_session(session_store.create(model: current_model_id, reasoning_effort: current_reasoning_effort))
|
|
778
|
+
reset_session_diff
|
|
779
|
+
conversation = new_conversation(workspace_root: session_store.cwd)
|
|
780
|
+
@active_session.attach(conversation)
|
|
781
|
+
build_interactive_agent(conversation)
|
|
782
|
+
end
|
|
783
|
+
|
|
322
784
|
def track_session(session)
|
|
323
785
|
@cleanup_sessions << session if session
|
|
324
786
|
session
|
|
@@ -328,12 +790,17 @@ module Kward
|
|
|
328
790
|
@session_diff = path ? SessionDiff.from_session_file(path) : SessionDiff.new
|
|
329
791
|
end
|
|
330
792
|
|
|
331
|
-
def update_session_diff(content)
|
|
793
|
+
def update_session_diff(content, tool_call: nil)
|
|
794
|
+
return unless mutation_tool_call?(tool_call)
|
|
332
795
|
return unless @session_diff&.add_tool_result(content)
|
|
333
796
|
|
|
334
797
|
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
335
798
|
end
|
|
336
799
|
|
|
800
|
+
def mutation_tool_call?(tool_call)
|
|
801
|
+
["edit_file", "write_file", "edit", "write"].include?(ToolCall.name(tool_call).to_s)
|
|
802
|
+
end
|
|
803
|
+
|
|
337
804
|
def cleanup_unused_sessions
|
|
338
805
|
@cleanup_sessions.reverse_each do |session|
|
|
339
806
|
session.delete_if_unused if session.respond_to?(:delete_if_unused)
|
|
@@ -348,7 +815,7 @@ module Kward
|
|
|
348
815
|
previous_session.delete_if_unused if previous_session.respond_to?(:delete_if_unused)
|
|
349
816
|
end
|
|
350
817
|
|
|
351
|
-
def new_conversation(workspace_root:
|
|
818
|
+
def new_conversation(workspace_root: current_workspace_root)
|
|
352
819
|
Conversation.new(workspace_root: workspace_root, model: current_model_id, reasoning_effort: current_reasoning_effort, plugin_registry: plugin_registry)
|
|
353
820
|
end
|
|
354
821
|
|
|
@@ -375,7 +842,7 @@ module Kward
|
|
|
375
842
|
|
|
376
843
|
def build_interactive_agent(conversation)
|
|
377
844
|
conversation.plugin_registry ||= plugin_registry if conversation.respond_to?(:plugin_registry)
|
|
378
|
-
workspace =
|
|
845
|
+
workspace = configured_workspace(root: conversation.workspace_root)
|
|
379
846
|
tool_registry = ToolRegistry.new(workspace: workspace, prompt: @prompt)
|
|
380
847
|
@footer_conversation = conversation
|
|
381
848
|
@footer_tool_registry = tool_registry
|
|
@@ -386,6 +853,39 @@ module Kward
|
|
|
386
853
|
)
|
|
387
854
|
end
|
|
388
855
|
|
|
856
|
+
def handle_interactive_shell_command(input, agent)
|
|
857
|
+
command = input.to_s.sub(/\A!\s*/, "")
|
|
858
|
+
if command.strip.empty?
|
|
859
|
+
@prompt.say("\nShell command is required after !\n")
|
|
860
|
+
return true
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
run_busy_local_command_and_requeue(activity: "running") do
|
|
864
|
+
result = configured_workspace(root: interactive_workspace_root(agent)).run_shell_command(command)
|
|
865
|
+
@prompt.say("\n#{colored("Shell>", :green, :bold)} #{command}\n#{result}\n")
|
|
866
|
+
end
|
|
867
|
+
true
|
|
868
|
+
end
|
|
869
|
+
|
|
870
|
+
def shell_command_input?(input)
|
|
871
|
+
input.to_s.start_with?("!")
|
|
872
|
+
end
|
|
873
|
+
|
|
874
|
+
def configured_workspace(root: current_workspace_root)
|
|
875
|
+
Workspace.new(root: root, guardrails: workspace_guardrails_enabled?)
|
|
876
|
+
end
|
|
877
|
+
|
|
878
|
+
def workspace_guardrails_enabled?
|
|
879
|
+
ConfigFiles.workspace_guardrails_enabled?(safely_read_config.to_h)
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
def interactive_workspace_root(agent)
|
|
883
|
+
conversation = agent.conversation if agent.respond_to?(:conversation)
|
|
884
|
+
return conversation.workspace_root if conversation&.respond_to?(:workspace_root)
|
|
885
|
+
|
|
886
|
+
current_workspace_root
|
|
887
|
+
end
|
|
888
|
+
|
|
389
889
|
def handle_local_slash_command(command, agent, session_store)
|
|
390
890
|
name, argument = parse_slash_command(command)
|
|
391
891
|
case name
|
|
@@ -395,9 +895,6 @@ module Kward
|
|
|
395
895
|
when "stats"
|
|
396
896
|
run_busy_local_command_and_requeue { print_stats(argument) }
|
|
397
897
|
[true, nil]
|
|
398
|
-
when "crew"
|
|
399
|
-
@prompt.say("\nThe /crew command is not implemented yet.\n")
|
|
400
|
-
[true, nil]
|
|
401
898
|
when "memory"
|
|
402
899
|
activity = memory_summarize_command?(argument) ? "summarizing" : "loading"
|
|
403
900
|
run_busy_local_command_and_requeue(activity: activity) { handle_memory_command(argument, agent) }
|
|
@@ -406,7 +903,7 @@ module Kward
|
|
|
406
903
|
run_busy_local_command_and_requeue { @prompt.redraw if @prompt.respond_to?(:redraw) }
|
|
407
904
|
[true, nil]
|
|
408
905
|
when "settings"
|
|
409
|
-
configure_settings
|
|
906
|
+
configure_settings(agent.conversation)
|
|
410
907
|
[true, nil]
|
|
411
908
|
when "login"
|
|
412
909
|
login_interactively
|
|
@@ -421,6 +918,9 @@ module Kward
|
|
|
421
918
|
when "reasoning"
|
|
422
919
|
configure_reasoning(agent.conversation)
|
|
423
920
|
[true, nil]
|
|
921
|
+
when "reload"
|
|
922
|
+
run_busy_local_command_and_requeue { reload_plugins(agent.conversation) }
|
|
923
|
+
[true, nil]
|
|
424
924
|
when "new"
|
|
425
925
|
[true, run_busy_local_command_and_requeue { start_new_session(session_store) }]
|
|
426
926
|
when "resume"
|
|
@@ -434,6 +934,8 @@ module Kward
|
|
|
434
934
|
[true, nil]
|
|
435
935
|
when "clone"
|
|
436
936
|
[true, run_busy_local_command_and_requeue { clone_session(session_store, agent) }]
|
|
937
|
+
when "tree"
|
|
938
|
+
[true, run_busy_local_command_and_requeue { navigate_session_tree(session_store) }]
|
|
437
939
|
when "copy"
|
|
438
940
|
run_busy_local_command_and_requeue { copy_session_text(agent.conversation, argument) }
|
|
439
941
|
[true, nil]
|
|
@@ -528,13 +1030,16 @@ module Kward
|
|
|
528
1030
|
record = manager.add_soft(unquote_argument(rest), scope: "workspace:#{agent.conversation.workspace_root}")
|
|
529
1031
|
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Added soft memory #{record["id"]}.\n")
|
|
530
1032
|
when "list"
|
|
531
|
-
@prompt.say("\n#{format_memory_list(manager.
|
|
1033
|
+
@prompt.say("\n#{format_memory_list(manager.hierarchy(workspace_root: agent.conversation.workspace_root))}\n")
|
|
532
1034
|
when "forget"
|
|
533
1035
|
forgotten = manager.forget_memory(rest.to_s.strip)
|
|
534
1036
|
@prompt.say("\n#{forgotten ? "Forgot #{rest.to_s.strip}." : "No memory found for #{rest.to_s.strip}."}\n")
|
|
535
1037
|
when "promote"
|
|
536
|
-
record = manager.
|
|
537
|
-
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Promoted
|
|
1038
|
+
record = manager.promote_memory(rest.to_s.strip)
|
|
1039
|
+
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Promoted memory #{record["id"]}.\n")
|
|
1040
|
+
when "relax"
|
|
1041
|
+
record = manager.relax_core(rest.to_s.strip, workspace_root: agent.conversation.workspace_root)
|
|
1042
|
+
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Relaxed memory #{record["id"]}.\n")
|
|
538
1043
|
when "inspect"
|
|
539
1044
|
@prompt.say("\n#{JSON.pretty_generate(manager.inspect_memory)}\n")
|
|
540
1045
|
when "why"
|
|
@@ -544,7 +1049,7 @@ module Kward
|
|
|
544
1049
|
records = summarize_memory(agent.conversation, manager: manager)
|
|
545
1050
|
@prompt.say("\n#{colored(assistant_output_prompt, :green, :bold)} Learned #{records.length} soft #{records.length == 1 ? "memory" : "memories"}.\n")
|
|
546
1051
|
else
|
|
547
|
-
@prompt.say("\nUsage: /memory enable|disable|auto-summary enable|disable|core <text>|add <text>|list|forget <id>|promote <id>|inspect|why|summarize\n")
|
|
1052
|
+
@prompt.say("\nUsage: /memory enable|disable|auto-summary enable|disable|core <text>|add <text>|list|forget <id>|promote <id>|relax <id>|inspect|why|summarize\n")
|
|
548
1053
|
end
|
|
549
1054
|
rescue StandardError => e
|
|
550
1055
|
@prompt.say("\nMemory command failed: #{e.message}\n")
|
|
@@ -563,13 +1068,18 @@ module Kward
|
|
|
563
1068
|
end
|
|
564
1069
|
|
|
565
1070
|
def format_memory_list(memories)
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
1071
|
+
sections = [
|
|
1072
|
+
["Global Core Memories:", Array(memories["global_core"])],
|
|
1073
|
+
["Workspace Core Memories:", Array(memories["workspace_core"])],
|
|
1074
|
+
["Workspace Soft Memories:", Array(memories["workspace_soft"])]
|
|
1075
|
+
]
|
|
1076
|
+
|
|
1077
|
+
sections.flat_map do |heading, records|
|
|
1078
|
+
lines = [heading]
|
|
1079
|
+
records.each { |item| lines << "- #{item["id"]} [#{item["scope"]}] #{item["text"]}" }
|
|
1080
|
+
lines << "- none" if records.empty?
|
|
1081
|
+
lines
|
|
1082
|
+
end.join("\n")
|
|
573
1083
|
end
|
|
574
1084
|
|
|
575
1085
|
def format_memory_why(explanation)
|
|
@@ -616,33 +1126,478 @@ module Kward
|
|
|
616
1126
|
|
|
617
1127
|
def current_workspace_root
|
|
618
1128
|
return @active_session.cwd.to_s unless @active_session&.cwd.to_s.empty?
|
|
1129
|
+
return @working_directory if @working_directory
|
|
619
1130
|
|
|
620
1131
|
Dir.pwd
|
|
621
1132
|
end
|
|
622
1133
|
|
|
623
|
-
def configure_settings
|
|
1134
|
+
def configure_settings(conversation = nil)
|
|
624
1135
|
unless settings_overlay_available?
|
|
625
1136
|
@prompt.say("\nSettings overlay is unavailable in this prompt.\n")
|
|
626
1137
|
return
|
|
627
1138
|
end
|
|
628
1139
|
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
settings = ConfigFiles.update_overlay_settings("alignment" => alignment)
|
|
634
|
-
@prompt.update_overlay_settings(settings)
|
|
1140
|
+
loop do
|
|
1141
|
+
selected = @prompt.select("Settings category", settings_category_choices, title: "Settings")
|
|
1142
|
+
category = selected_settings_category(selected)
|
|
1143
|
+
break unless category
|
|
635
1144
|
|
|
636
|
-
|
|
637
|
-
return unless width
|
|
1145
|
+
break if category == "done"
|
|
638
1146
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
@prompt.say("\nSaved overlay settings.\n")
|
|
1147
|
+
handle_settings_category(category, conversation)
|
|
1148
|
+
end
|
|
642
1149
|
rescue StandardError => e
|
|
643
1150
|
@prompt.say("\nSettings error: #{e.message}\n")
|
|
644
1151
|
end
|
|
645
1152
|
|
|
1153
|
+
def settings_category_choices
|
|
1154
|
+
[
|
|
1155
|
+
"Model & Reasoning",
|
|
1156
|
+
"Accounts",
|
|
1157
|
+
"Memory",
|
|
1158
|
+
"Interface",
|
|
1159
|
+
"Tools & Search",
|
|
1160
|
+
"Context & Compaction",
|
|
1161
|
+
"Personalization",
|
|
1162
|
+
"Logging",
|
|
1163
|
+
"Advanced",
|
|
1164
|
+
"Done"
|
|
1165
|
+
]
|
|
1166
|
+
end
|
|
1167
|
+
|
|
1168
|
+
def selected_settings_category(selected)
|
|
1169
|
+
text = selected.to_s.downcase
|
|
1170
|
+
return nil if text.empty?
|
|
1171
|
+
return "done" if text.start_with?("done")
|
|
1172
|
+
return "model" if text.start_with?("model")
|
|
1173
|
+
return "accounts" if text.start_with?("accounts")
|
|
1174
|
+
return "memory" if text.start_with?("memory")
|
|
1175
|
+
return "interface" if text.start_with?("interface")
|
|
1176
|
+
return "tools" if text.start_with?("tools")
|
|
1177
|
+
return "context" if text.start_with?("context")
|
|
1178
|
+
return "personalization" if text.start_with?("personalization")
|
|
1179
|
+
return "logging" if text.start_with?("logging")
|
|
1180
|
+
return "advanced" if text.start_with?("advanced")
|
|
1181
|
+
|
|
1182
|
+
nil
|
|
1183
|
+
end
|
|
1184
|
+
|
|
1185
|
+
def handle_settings_category(category, conversation)
|
|
1186
|
+
case category
|
|
1187
|
+
when "model"
|
|
1188
|
+
configure_model_settings(conversation)
|
|
1189
|
+
when "accounts"
|
|
1190
|
+
configure_account_settings
|
|
1191
|
+
when "memory"
|
|
1192
|
+
configure_memory_settings(conversation)
|
|
1193
|
+
when "interface"
|
|
1194
|
+
configure_interface_settings
|
|
1195
|
+
when "tools"
|
|
1196
|
+
configure_tools_settings
|
|
1197
|
+
when "context"
|
|
1198
|
+
configure_context_settings
|
|
1199
|
+
when "personalization"
|
|
1200
|
+
configure_personalization_settings(conversation)
|
|
1201
|
+
when "logging"
|
|
1202
|
+
configure_logging_settings
|
|
1203
|
+
when "advanced"
|
|
1204
|
+
show_advanced_settings
|
|
1205
|
+
end
|
|
1206
|
+
end
|
|
1207
|
+
|
|
1208
|
+
def configure_model_settings(conversation)
|
|
1209
|
+
selected = @prompt.select("Model & Reasoning", ["Provider", "Default model", "Reasoning effort", "Back"], title: "Settings")
|
|
1210
|
+
case selected.to_s.downcase
|
|
1211
|
+
when /\Aprovider/
|
|
1212
|
+
configure_provider(conversation)
|
|
1213
|
+
when /\Adefault model/
|
|
1214
|
+
configure_model(conversation)
|
|
1215
|
+
when /\Areasoning effort/
|
|
1216
|
+
configure_reasoning(conversation)
|
|
1217
|
+
end
|
|
1218
|
+
end
|
|
1219
|
+
|
|
1220
|
+
def configure_provider(conversation)
|
|
1221
|
+
selected = @prompt.select("Provider", provider_choices, title: "Settings")
|
|
1222
|
+
provider = selected_provider(selected)
|
|
1223
|
+
return unless provider
|
|
1224
|
+
|
|
1225
|
+
ConfigFiles.update_config("provider" => ModelInfo.config_provider_for_provider(provider))
|
|
1226
|
+
reload_client_config
|
|
1227
|
+
refresh_conversation_runtime(conversation)
|
|
1228
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
1229
|
+
end
|
|
1230
|
+
|
|
1231
|
+
def provider_choices
|
|
1232
|
+
current = current_model_provider
|
|
1233
|
+
["Codex", "OpenRouter", "Copilot"].map do |provider|
|
|
1234
|
+
label = provider.dup
|
|
1235
|
+
label += " (current)" if provider == current
|
|
1236
|
+
label
|
|
1237
|
+
end
|
|
1238
|
+
end
|
|
1239
|
+
|
|
1240
|
+
def selected_provider(selected)
|
|
1241
|
+
text = selected.to_s.downcase
|
|
1242
|
+
return "Codex" if text.start_with?("codex")
|
|
1243
|
+
return "OpenRouter" if text.start_with?("openrouter")
|
|
1244
|
+
return "Copilot" if text.start_with?("copilot")
|
|
1245
|
+
|
|
1246
|
+
nil
|
|
1247
|
+
end
|
|
1248
|
+
|
|
1249
|
+
def configure_account_settings
|
|
1250
|
+
selected = @prompt.select("Accounts", account_setting_choices, title: "Settings")
|
|
1251
|
+
case selected.to_s.downcase
|
|
1252
|
+
when /\Aopenai/
|
|
1253
|
+
login(provider: "openai")
|
|
1254
|
+
reload_client_config
|
|
1255
|
+
when /\Agithub/
|
|
1256
|
+
login(provider: "github")
|
|
1257
|
+
reload_client_config
|
|
1258
|
+
when /\Aopenrouter/
|
|
1259
|
+
login(provider: "openrouter")
|
|
1260
|
+
reload_client_config
|
|
1261
|
+
when /\Astatus/
|
|
1262
|
+
print_auth_status
|
|
1263
|
+
end
|
|
1264
|
+
end
|
|
1265
|
+
|
|
1266
|
+
def account_setting_choices
|
|
1267
|
+
config = safely_read_config.to_h
|
|
1268
|
+
[
|
|
1269
|
+
"OpenAI login (#{File.exist?(OpenAIOAuth.default_auth_path) ? "configured" : "not configured"})",
|
|
1270
|
+
"GitHub login (#{File.exist?(GithubOAuth.default_auth_path) ? "configured" : "not configured"})",
|
|
1271
|
+
"OpenRouter API key (#{openrouter_key_status(config)})",
|
|
1272
|
+
"Status",
|
|
1273
|
+
"Back"
|
|
1274
|
+
]
|
|
1275
|
+
end
|
|
1276
|
+
|
|
1277
|
+
def openrouter_key_status(config)
|
|
1278
|
+
return "configured via environment" unless ENV["OPENROUTER_API_KEY"].to_s.empty?
|
|
1279
|
+
|
|
1280
|
+
config["openrouter_api_key"].to_s.empty? ? "not configured" : "configured"
|
|
1281
|
+
end
|
|
1282
|
+
|
|
1283
|
+
def configure_memory_settings(conversation)
|
|
1284
|
+
selected = @prompt.select("Memory", memory_setting_choices, title: "Settings")
|
|
1285
|
+
case selected.to_s.downcase
|
|
1286
|
+
when /\Aenable memory/, /\Adisable memory/
|
|
1287
|
+
set_memory_enabled(!memory_enabled?)
|
|
1288
|
+
conversation&.refresh_system_message!
|
|
1289
|
+
@prompt.say("\nMemory #{memory_enabled? ? "enabled" : "disabled"}.\n")
|
|
1290
|
+
when /\Aenable auto-summary/, /\Adisable auto-summary/
|
|
1291
|
+
set_memory_auto_summary_enabled(!memory_auto_summary_enabled?)
|
|
1292
|
+
@prompt.say("\nMemory auto-summary #{memory_auto_summary_enabled? ? "enabled" : "disabled"}.\n")
|
|
1293
|
+
when /\Amanage/
|
|
1294
|
+
@prompt.say("\nUse /memory enable|disable|auto-summary enable|disable|core <text>|add <text>|list|forget <id>|promote <id>|relax <id>|inspect|why|summarize.\n")
|
|
1295
|
+
end
|
|
1296
|
+
end
|
|
1297
|
+
|
|
1298
|
+
def memory_setting_choices
|
|
1299
|
+
[
|
|
1300
|
+
"#{memory_enabled? ? "Disable" : "Enable"} memory (currently #{on_off(memory_enabled?)})",
|
|
1301
|
+
"#{memory_auto_summary_enabled? ? "Disable" : "Enable"} auto-summary (currently #{on_off(memory_auto_summary_enabled?)})",
|
|
1302
|
+
"Manage memories with /memory",
|
|
1303
|
+
"Back"
|
|
1304
|
+
]
|
|
1305
|
+
end
|
|
1306
|
+
|
|
1307
|
+
def memory_enabled?
|
|
1308
|
+
memory = safely_read_config.to_h["memory"]
|
|
1309
|
+
memory.is_a?(Hash) && memory["enabled"] == true
|
|
1310
|
+
end
|
|
1311
|
+
|
|
1312
|
+
def memory_auto_summary_enabled?
|
|
1313
|
+
memory = safely_read_config.to_h["memory"]
|
|
1314
|
+
memory.is_a?(Hash) && memory["auto_summary"] == true
|
|
1315
|
+
end
|
|
1316
|
+
|
|
1317
|
+
def set_memory_enabled(enabled)
|
|
1318
|
+
update_nested_config("memory", "enabled" => enabled)
|
|
1319
|
+
end
|
|
1320
|
+
|
|
1321
|
+
def set_memory_auto_summary_enabled(enabled)
|
|
1322
|
+
update_nested_config("memory", "auto_summary" => enabled)
|
|
1323
|
+
end
|
|
1324
|
+
|
|
1325
|
+
def configure_interface_settings
|
|
1326
|
+
selected = @prompt.select("Interface", interface_setting_choices, title: "Settings")
|
|
1327
|
+
case selected.to_s.downcase
|
|
1328
|
+
when /\Aoverlay alignment/
|
|
1329
|
+
settings = ConfigFiles.overlay_settings
|
|
1330
|
+
alignment = choose_overlay_setting("Overlay alignment", overlay_alignment_choices(settings), ConfigFiles::OVERLAY_ALIGNMENTS)
|
|
1331
|
+
return unless alignment
|
|
1332
|
+
|
|
1333
|
+
@prompt.update_overlay_settings(ConfigFiles.update_overlay_settings("alignment" => alignment))
|
|
1334
|
+
when /\Aoverlay width/
|
|
1335
|
+
settings = ConfigFiles.overlay_settings
|
|
1336
|
+
width = choose_overlay_setting("Overlay width", overlay_width_choices(settings), ConfigFiles::OVERLAY_WIDTHS)
|
|
1337
|
+
return unless width
|
|
1338
|
+
|
|
1339
|
+
@prompt.update_overlay_settings(ConfigFiles.update_overlay_settings("width" => width))
|
|
1340
|
+
when /\Ashow busy help/, /\Ahide busy help/
|
|
1341
|
+
set_composer_busy_help(!composer_busy_help?)
|
|
1342
|
+
@prompt.say("\nBusy help #{composer_busy_help? ? "enabled" : "disabled"}. Restart the TUI to apply this setting.\n")
|
|
1343
|
+
when /\Ashow startup banner/, /\Ahide startup banner/
|
|
1344
|
+
set_banner_enabled(!banner_enabled?)
|
|
1345
|
+
@prompt.say("\nStartup banner #{banner_enabled? ? "enabled" : "disabled"}. Restart the TUI to apply this setting.\n")
|
|
1346
|
+
when /\Aenable session auto-resume/, /\Adisable session auto-resume/
|
|
1347
|
+
set_session_auto_resume_enabled(!session_auto_resume_enabled?)
|
|
1348
|
+
@prompt.say("\nSession auto-resume #{session_auto_resume_enabled? ? "enabled" : "disabled"}.\n")
|
|
1349
|
+
end
|
|
1350
|
+
end
|
|
1351
|
+
|
|
1352
|
+
def interface_setting_choices
|
|
1353
|
+
settings = ConfigFiles.overlay_settings
|
|
1354
|
+
[
|
|
1355
|
+
"Overlay alignment (#{settings["alignment"]})",
|
|
1356
|
+
"Overlay width (#{settings["width"]})",
|
|
1357
|
+
"#{composer_busy_help? ? "Hide" : "Show"} busy help (currently #{on_off(composer_busy_help?)})",
|
|
1358
|
+
"#{banner_enabled? ? "Hide" : "Show"} startup banner (currently #{on_off(banner_enabled?)})",
|
|
1359
|
+
"#{session_auto_resume_enabled? ? "Disable" : "Enable"} session auto-resume (currently #{on_off(session_auto_resume_enabled?)})",
|
|
1360
|
+
"Back"
|
|
1361
|
+
]
|
|
1362
|
+
end
|
|
1363
|
+
|
|
1364
|
+
def composer_busy_help?
|
|
1365
|
+
ConfigFiles.composer_busy_help?(safely_read_config.to_h)
|
|
1366
|
+
end
|
|
1367
|
+
|
|
1368
|
+
def banner_enabled?
|
|
1369
|
+
ConfigFiles.banner_enabled?(safely_read_config.to_h)
|
|
1370
|
+
end
|
|
1371
|
+
|
|
1372
|
+
def session_auto_resume_enabled?
|
|
1373
|
+
ConfigFiles.session_auto_resume_enabled?(safely_read_config.to_h)
|
|
1374
|
+
end
|
|
1375
|
+
|
|
1376
|
+
def set_composer_busy_help(enabled)
|
|
1377
|
+
update_nested_config("composer", "busy_help" => enabled)
|
|
1378
|
+
end
|
|
1379
|
+
|
|
1380
|
+
def set_banner_enabled(enabled)
|
|
1381
|
+
update_nested_config("banner", "enabled" => enabled)
|
|
1382
|
+
end
|
|
1383
|
+
|
|
1384
|
+
def set_session_auto_resume_enabled(enabled)
|
|
1385
|
+
update_nested_config("sessions", "auto_resume" => enabled)
|
|
1386
|
+
end
|
|
1387
|
+
|
|
1388
|
+
def configure_tools_settings
|
|
1389
|
+
selected = @prompt.select("Tools & Search", tools_setting_choices, title: "Settings")
|
|
1390
|
+
case selected.to_s.downcase
|
|
1391
|
+
when /\Aenable web search/, /\Adisable web search/
|
|
1392
|
+
set_web_search_enabled(!web_search_enabled?)
|
|
1393
|
+
@prompt.say("\nWeb search #{web_search_enabled? ? "enabled" : "disabled"}.\n")
|
|
1394
|
+
when /\Aweb search provider/
|
|
1395
|
+
configure_web_search_provider
|
|
1396
|
+
when /\Aallow model-provider/, /\Adisallow model-provider/
|
|
1397
|
+
set_web_search_allow_model_providers(!web_search_allow_model_providers?)
|
|
1398
|
+
@prompt.say("\nModel-provider web search #{web_search_allow_model_providers? ? "enabled" : "disabled"}.\n")
|
|
1399
|
+
when /\Aenable workspace guardrails/, /\Adisable workspace guardrails/
|
|
1400
|
+
set_workspace_guardrails_enabled(!workspace_guardrails_enabled?)
|
|
1401
|
+
@prompt.say("\nWorkspace guardrails #{workspace_guardrails_enabled? ? "enabled" : "disabled"}.\n")
|
|
1402
|
+
end
|
|
1403
|
+
end
|
|
1404
|
+
|
|
1405
|
+
def tools_setting_choices
|
|
1406
|
+
[
|
|
1407
|
+
"#{web_search_enabled? ? "Disable" : "Enable"} web search (currently #{on_off(web_search_enabled?)})",
|
|
1408
|
+
"Web search provider (#{web_search_provider})",
|
|
1409
|
+
"#{web_search_allow_model_providers? ? "Disallow" : "Allow"} model-provider web search (currently #{on_off(web_search_allow_model_providers?)})",
|
|
1410
|
+
"#{workspace_guardrails_enabled? ? "Disable" : "Enable"} workspace guardrails (currently #{on_off(workspace_guardrails_enabled?)})",
|
|
1411
|
+
"Back"
|
|
1412
|
+
]
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1415
|
+
def configure_web_search_provider
|
|
1416
|
+
providers = %w[auto exa perplexity gemini legacy]
|
|
1417
|
+
selected = @prompt.select("Web search provider", providers.map { |provider| provider == web_search_provider ? "#{provider} (current)" : provider }, title: "Settings")
|
|
1418
|
+
provider = providers.find { |value| selected.to_s.downcase.start_with?(value) }
|
|
1419
|
+
return unless provider
|
|
1420
|
+
|
|
1421
|
+
update_nested_config("web_search", "provider" => provider)
|
|
1422
|
+
end
|
|
1423
|
+
|
|
1424
|
+
def web_search_config
|
|
1425
|
+
config = safely_read_config.to_h
|
|
1426
|
+
value = config["web_search"] || config["webSearch"] || config["web_research"] || config["webResearch"]
|
|
1427
|
+
value.is_a?(Hash) ? value : {}
|
|
1428
|
+
end
|
|
1429
|
+
|
|
1430
|
+
def web_search_enabled?
|
|
1431
|
+
web_search_config["enabled"] != false
|
|
1432
|
+
end
|
|
1433
|
+
|
|
1434
|
+
def web_search_provider
|
|
1435
|
+
web_search_config["provider"].to_s.empty? ? "auto" : web_search_config["provider"].to_s
|
|
1436
|
+
end
|
|
1437
|
+
|
|
1438
|
+
def web_search_allow_model_providers?
|
|
1439
|
+
web_search_config["allow_model_providers"] == true
|
|
1440
|
+
end
|
|
1441
|
+
|
|
1442
|
+
def set_web_search_enabled(enabled)
|
|
1443
|
+
update_nested_config("web_search", "enabled" => enabled)
|
|
1444
|
+
end
|
|
1445
|
+
|
|
1446
|
+
def set_web_search_allow_model_providers(enabled)
|
|
1447
|
+
update_nested_config("web_search", "allow_model_providers" => enabled)
|
|
1448
|
+
end
|
|
1449
|
+
|
|
1450
|
+
def set_workspace_guardrails_enabled(enabled)
|
|
1451
|
+
update_nested_config("tools", "workspace_guardrails" => enabled)
|
|
1452
|
+
end
|
|
1453
|
+
|
|
1454
|
+
def configure_context_settings
|
|
1455
|
+
selected = @prompt.select("Context & Compaction", context_setting_choices, title: "Settings")
|
|
1456
|
+
case selected.to_s.downcase
|
|
1457
|
+
when /\Aenable auto-compaction/, /\Adisable auto-compaction/
|
|
1458
|
+
set_compaction_enabled(!compaction_enabled?)
|
|
1459
|
+
@prompt.say("\nAuto-compaction #{compaction_enabled? ? "enabled" : "disabled"}.\n")
|
|
1460
|
+
else
|
|
1461
|
+
@prompt.say("\n#{auto_compaction_status_line}\n") if selected.to_s.downcase.start_with?("status")
|
|
1462
|
+
end
|
|
1463
|
+
end
|
|
1464
|
+
|
|
1465
|
+
def context_setting_choices
|
|
1466
|
+
[
|
|
1467
|
+
"#{compaction_enabled? ? "Disable" : "Enable"} auto-compaction (currently #{on_off(compaction_enabled?)})",
|
|
1468
|
+
"Status",
|
|
1469
|
+
"Back"
|
|
1470
|
+
]
|
|
1471
|
+
end
|
|
1472
|
+
|
|
1473
|
+
def compaction_enabled?
|
|
1474
|
+
Kward::Compaction::Settings.from_config(safely_read_config.to_h).enabled
|
|
1475
|
+
end
|
|
1476
|
+
|
|
1477
|
+
def set_compaction_enabled(enabled)
|
|
1478
|
+
update_nested_config("compaction", "enabled" => enabled)
|
|
1479
|
+
end
|
|
1480
|
+
|
|
1481
|
+
def configure_personalization_settings(conversation)
|
|
1482
|
+
selected = @prompt.select("Personalization", personalization_setting_choices(conversation), title: "Settings")
|
|
1483
|
+
case selected.to_s.downcase
|
|
1484
|
+
when /\Adefault persona/
|
|
1485
|
+
configure_default_persona(conversation)
|
|
1486
|
+
when /\Aactive instructions/
|
|
1487
|
+
show_active_instructions_summary(conversation)
|
|
1488
|
+
end
|
|
1489
|
+
end
|
|
1490
|
+
|
|
1491
|
+
def personalization_setting_choices(conversation)
|
|
1492
|
+
[
|
|
1493
|
+
"Default persona (#{default_persona_label})",
|
|
1494
|
+
"Active instructions summary",
|
|
1495
|
+
"Back"
|
|
1496
|
+
]
|
|
1497
|
+
end
|
|
1498
|
+
|
|
1499
|
+
def default_persona_label
|
|
1500
|
+
personas = safely_read_config.to_h["personas"]
|
|
1501
|
+
value = personas.is_a?(Hash) ? personas["default"] : nil
|
|
1502
|
+
value.to_s.empty? ? "none" : value.to_s
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1505
|
+
def configure_default_persona(conversation)
|
|
1506
|
+
config = safely_read_config.to_h
|
|
1507
|
+
personas = config["personas"].is_a?(Hash) ? config["personas"] : {}
|
|
1508
|
+
entries = ConfigFiles.crew_character_labels(personas)
|
|
1509
|
+
choices = entries.map { |key, label| key == personas["default"] ? "#{label} (#{key}, current)" : "#{label} (#{key})" }
|
|
1510
|
+
if choices.empty?
|
|
1511
|
+
@prompt.say("\nNo configured personas found. Edit #{ConfigFiles.config_path} to add personas.\n")
|
|
1512
|
+
return
|
|
1513
|
+
end
|
|
1514
|
+
|
|
1515
|
+
selected = @prompt.select("Default persona", choices, title: "Settings")
|
|
1516
|
+
key = entries.keys.find { |candidate| selected.to_s.include?("(#{candidate}") }
|
|
1517
|
+
return unless key
|
|
1518
|
+
|
|
1519
|
+
personas = personas.dup
|
|
1520
|
+
personas["default"] = key
|
|
1521
|
+
ConfigFiles.update_config("personas" => personas)
|
|
1522
|
+
conversation&.refresh_system_message!
|
|
1523
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
1524
|
+
end
|
|
1525
|
+
|
|
1526
|
+
def show_active_instructions_summary(conversation)
|
|
1527
|
+
label = ConfigFiles.active_persona_label(workspace_root: current_workspace_root, model: current_model_id, config: safely_read_config.to_h)
|
|
1528
|
+
lines = ["Active persona: #{label || "none"}"]
|
|
1529
|
+
lines << "Global AGENTS.md: #{ConfigFiles.agents_prompt ? "present" : "absent"}"
|
|
1530
|
+
lines << "Workspace AGENTS.md: #{ConfigFiles.workspace_agents_prompt(current_workspace_root) ? "present" : "absent"}"
|
|
1531
|
+
lines << "Messages: #{conversation.messages.length}" if conversation&.respond_to?(:messages)
|
|
1532
|
+
@prompt.say("\n#{lines.join("\n")}\n")
|
|
1533
|
+
end
|
|
1534
|
+
|
|
1535
|
+
def configure_logging_settings
|
|
1536
|
+
selected = @prompt.select("Logging", logging_setting_choices, title: "Settings")
|
|
1537
|
+
key = logging_key_for_choice(selected)
|
|
1538
|
+
return unless key
|
|
1539
|
+
|
|
1540
|
+
set_logging_value(key, !logging_enabled?(key))
|
|
1541
|
+
@prompt.say("\nLogging #{key.tr("_", " ")} #{logging_enabled?(key) ? "enabled" : "disabled"}.\n")
|
|
1542
|
+
end
|
|
1543
|
+
|
|
1544
|
+
def logging_setting_choices
|
|
1545
|
+
[
|
|
1546
|
+
"#{logging_enabled?("enabled") ? "Disable" : "Enable"} local logging (currently #{on_off(logging_enabled?("enabled"))})",
|
|
1547
|
+
"#{logging_enabled?("tokens") ? "Disable" : "Enable"} token logs (currently #{on_off(logging_enabled?("tokens"))})",
|
|
1548
|
+
"#{logging_enabled?("performance") ? "Disable" : "Enable"} performance logs (currently #{on_off(logging_enabled?("performance"))})",
|
|
1549
|
+
"#{logging_enabled?("tools") ? "Disable" : "Enable"} tool logs (currently #{on_off(logging_enabled?("tools"))})",
|
|
1550
|
+
"#{logging_enabled?("errors") ? "Disable" : "Enable"} error logs (currently #{on_off(logging_enabled?("errors"))})",
|
|
1551
|
+
"Back"
|
|
1552
|
+
]
|
|
1553
|
+
end
|
|
1554
|
+
|
|
1555
|
+
def logging_key_for_choice(selected)
|
|
1556
|
+
text = selected.to_s.downcase
|
|
1557
|
+
return "enabled" if text.include?("local logging")
|
|
1558
|
+
return "tokens" if text.include?("token logs")
|
|
1559
|
+
return "performance" if text.include?("performance logs")
|
|
1560
|
+
return "tools" if text.include?("tool logs")
|
|
1561
|
+
return "errors" if text.include?("error logs")
|
|
1562
|
+
|
|
1563
|
+
nil
|
|
1564
|
+
end
|
|
1565
|
+
|
|
1566
|
+
def logging_enabled?(key)
|
|
1567
|
+
logging = safely_read_config.to_h["logging"]
|
|
1568
|
+
logging.is_a?(Hash) && logging[key] == true
|
|
1569
|
+
end
|
|
1570
|
+
|
|
1571
|
+
def set_logging_value(key, value)
|
|
1572
|
+
update_nested_config("logging", key => value)
|
|
1573
|
+
end
|
|
1574
|
+
|
|
1575
|
+
def show_advanced_settings
|
|
1576
|
+
lines = [
|
|
1577
|
+
"Config path: #{ConfigFiles.config_path}",
|
|
1578
|
+
"Config directory: #{ConfigFiles.config_dir}",
|
|
1579
|
+
"Cache directory: #{ConfigFiles.cache_dir}",
|
|
1580
|
+
"Memory directory: #{ConfigFiles.memory_dir}",
|
|
1581
|
+
"Plugin directory: #{ConfigFiles.plugin_dir}",
|
|
1582
|
+
"Plugins: #{ConfigFiles.plugin_paths.length}",
|
|
1583
|
+
"Skills: #{ConfigFiles.skills.length}",
|
|
1584
|
+
"Prompt templates: #{ConfigFiles.prompt_templates(reserved_commands: BUILTIN_SLASH_COMMAND_NAMES).length}"
|
|
1585
|
+
]
|
|
1586
|
+
@prompt.say("\n#{lines.join("\n")}\n")
|
|
1587
|
+
end
|
|
1588
|
+
|
|
1589
|
+
def update_nested_config(section, values)
|
|
1590
|
+
config = ConfigFiles.read_config
|
|
1591
|
+
current = config[section].is_a?(Hash) ? config[section].dup : {}
|
|
1592
|
+
config[section] = current.merge(values)
|
|
1593
|
+
ConfigFiles.write_config(config)
|
|
1594
|
+
config
|
|
1595
|
+
end
|
|
1596
|
+
|
|
1597
|
+
def on_off(value)
|
|
1598
|
+
value ? "on" : "off"
|
|
1599
|
+
end
|
|
1600
|
+
|
|
646
1601
|
def login_interactively
|
|
647
1602
|
unless login_picker_available?
|
|
648
1603
|
@prompt.say("\nLogin provider picker is unavailable in this prompt.\n")
|
|
@@ -862,7 +1817,7 @@ module Kward
|
|
|
862
1817
|
return nil if path.to_s.empty?
|
|
863
1818
|
|
|
864
1819
|
previous_session = @active_session
|
|
865
|
-
@active_session, conversation = session_store.load(path, workspace:
|
|
1820
|
+
@active_session, conversation = session_store.load(path, workspace: configured_workspace(root: session_store.cwd), model: current_model_id, reasoning_effort: current_reasoning_effort)
|
|
866
1821
|
reset_session_diff(@active_session.path)
|
|
867
1822
|
track_session(@active_session)
|
|
868
1823
|
cleanup_replaced_session(previous_session)
|
|
@@ -879,6 +1834,92 @@ module Kward
|
|
|
879
1834
|
nil
|
|
880
1835
|
end
|
|
881
1836
|
|
|
1837
|
+
|
|
1838
|
+
def navigate_session_tree(session_store)
|
|
1839
|
+
return say_sessions_unavailable unless session_store
|
|
1840
|
+
unless @active_session
|
|
1841
|
+
@prompt.say("\nNo active persisted session.\n")
|
|
1842
|
+
return nil
|
|
1843
|
+
end
|
|
1844
|
+
|
|
1845
|
+
tree_items = session_tree_items(session_store)
|
|
1846
|
+
if tree_items.empty?
|
|
1847
|
+
@prompt.say("\nNo session tree entries found.\n")
|
|
1848
|
+
return nil
|
|
1849
|
+
end
|
|
1850
|
+
|
|
1851
|
+
labels_by_entry_id = tree_items.to_h { |item| [item[:entry]["id"].to_s, item[:label]] }
|
|
1852
|
+
current_leaf_id = @active_session.leaf_id || session_store.current_leaf(@active_session.path)
|
|
1853
|
+
initial_index = tree_items.index { |item| item[:entry]["id"].to_s == current_leaf_id.to_s } || tree_items.length - 1
|
|
1854
|
+
choice = select_session_tree_entry(labels_by_entry_id.values, initial_index: initial_index)
|
|
1855
|
+
return nil unless choice
|
|
1856
|
+
|
|
1857
|
+
entry_id = labels_by_entry_id.key(choice)
|
|
1858
|
+
entry = tree_items.find { |item| item[:entry]["id"].to_s == entry_id }&.fetch(:entry)
|
|
1859
|
+
return nil unless entry
|
|
1860
|
+
|
|
1861
|
+
selected_text = apply_session_tree_entry(entry)
|
|
1862
|
+
@prompt.say("\nMoved session tree position to #{entry["id"]}.\n")
|
|
1863
|
+
if selected_text && !selected_text.empty?
|
|
1864
|
+
if @prompt.respond_to?(:prefill_input)
|
|
1865
|
+
@prompt.prefill_input(selected_text)
|
|
1866
|
+
else
|
|
1867
|
+
@prompt.say("\nSelected text for editing:\n#{selected_text}\n")
|
|
1868
|
+
end
|
|
1869
|
+
end
|
|
1870
|
+
agent = reload_active_session(session_store)
|
|
1871
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
1872
|
+
agent
|
|
1873
|
+
rescue StandardError => e
|
|
1874
|
+
@prompt.say("\nSession tree error: #{e.message}\n")
|
|
1875
|
+
nil
|
|
1876
|
+
end
|
|
1877
|
+
|
|
1878
|
+
def select_session_tree_entry(labels, initial_index: 0)
|
|
1879
|
+
if @prompt.respond_to?(:select)
|
|
1880
|
+
return @prompt.select("Tree>", labels, title: "Session Tree", initial_index: initial_index)
|
|
1881
|
+
end
|
|
1882
|
+
|
|
1883
|
+
numbered_labels = labels.each_with_index.map { |label, index| "#{index + 1}. #{label}" }
|
|
1884
|
+
@prompt.say("\nSession tree:\n#{numbered_labels.join("\n")}\n")
|
|
1885
|
+
answer = @prompt.ask("Tree entry number>").to_s.strip
|
|
1886
|
+
answer.match?(/\A\d+\z/) ? labels[answer.to_i - 1] : nil
|
|
1887
|
+
end
|
|
1888
|
+
|
|
1889
|
+
def apply_session_tree_entry(entry)
|
|
1890
|
+
message = entry["message"]
|
|
1891
|
+
if message.is_a?(Hash) && message_role(message) == "user"
|
|
1892
|
+
target_leaf = entry["parentId"]
|
|
1893
|
+
target_leaf.to_s.empty? ? @active_session.reset_leaf : @active_session.branch(target_leaf)
|
|
1894
|
+
return full_message_text(message)
|
|
1895
|
+
end
|
|
1896
|
+
|
|
1897
|
+
@active_session.branch(entry["id"])
|
|
1898
|
+
nil
|
|
1899
|
+
end
|
|
1900
|
+
|
|
1901
|
+
def reload_active_session(session_store)
|
|
1902
|
+
@active_session, conversation = session_store.load(
|
|
1903
|
+
@active_session.path,
|
|
1904
|
+
workspace: configured_workspace(root: session_store.cwd),
|
|
1905
|
+
model: current_model_id,
|
|
1906
|
+
reasoning_effort: current_reasoning_effort
|
|
1907
|
+
)
|
|
1908
|
+
reset_session_diff(@active_session.path)
|
|
1909
|
+
track_session(@active_session)
|
|
1910
|
+
update_assistant_prompt(conversation)
|
|
1911
|
+
restore_prompt_transcript do
|
|
1912
|
+
render_conversation_transcript(conversation)
|
|
1913
|
+
end
|
|
1914
|
+
build_interactive_agent(conversation)
|
|
1915
|
+
end
|
|
1916
|
+
|
|
1917
|
+
def session_tree_items(session_store)
|
|
1918
|
+
roots = session_store.session_tree(@active_session.path)
|
|
1919
|
+
current_leaf_id = @active_session.leaf_id || session_store.current_leaf(@active_session.path)
|
|
1920
|
+
SessionTreeRenderer.new(roots: roots, current_leaf_id: current_leaf_id).items
|
|
1921
|
+
end
|
|
1922
|
+
|
|
882
1923
|
def rename_session(argument)
|
|
883
1924
|
unless @active_session
|
|
884
1925
|
@prompt.say("\nNo active persisted session.\n")
|
|
@@ -931,6 +1972,10 @@ module Kward
|
|
|
931
1972
|
nil
|
|
932
1973
|
end
|
|
933
1974
|
|
|
1975
|
+
def full_message_text(message)
|
|
1976
|
+
CLITranscriptFormatter.full_text(message)
|
|
1977
|
+
end
|
|
1978
|
+
|
|
934
1979
|
def copy_target_content(conversation, target)
|
|
935
1980
|
case target
|
|
936
1981
|
when "last"
|
|
@@ -946,7 +1991,7 @@ module Kward
|
|
|
946
1991
|
message = conversation.messages.reverse.find { |item| message_role(item) == "assistant" }
|
|
947
1992
|
return "" unless message
|
|
948
1993
|
|
|
949
|
-
|
|
1994
|
+
CLITranscriptFormatter.content_text(message_content(message))
|
|
950
1995
|
end
|
|
951
1996
|
|
|
952
1997
|
def copy_target_label(target)
|
|
@@ -978,10 +2023,10 @@ module Kward
|
|
|
978
2023
|
case role
|
|
979
2024
|
when "user"
|
|
980
2025
|
print_user_transcript(
|
|
981
|
-
|
|
982
|
-
display_input:
|
|
983
|
-
attachment_references:
|
|
984
|
-
image_parts:
|
|
2026
|
+
CLITranscriptFormatter.user_transcript_input(message),
|
|
2027
|
+
display_input: CLITranscriptFormatter.user_display_text(message),
|
|
2028
|
+
attachment_references: CLITranscriptFormatter.image_references(message),
|
|
2029
|
+
image_parts: CLITranscriptFormatter.image_parts(message)
|
|
985
2030
|
)
|
|
986
2031
|
when "assistant"
|
|
987
2032
|
render_reasoning(message)
|
|
@@ -995,25 +2040,25 @@ module Kward
|
|
|
995
2040
|
when "compactionSummary"
|
|
996
2041
|
render_transcript_block("Compaction summary", message_summary(message))
|
|
997
2042
|
else
|
|
998
|
-
render_transcript_block(role.to_s.capitalize,
|
|
2043
|
+
render_transcript_block(role.to_s.capitalize, CLITranscriptFormatter.content_text(message_content(message)))
|
|
999
2044
|
end
|
|
1000
2045
|
end
|
|
1001
2046
|
end
|
|
1002
2047
|
|
|
1003
2048
|
def render_reasoning(message)
|
|
1004
|
-
reasoning =
|
|
2049
|
+
reasoning = CLITranscriptFormatter.reasoning(message)
|
|
1005
2050
|
render_transcript_block("Reasoning", reasoning) unless reasoning.empty?
|
|
1006
2051
|
end
|
|
1007
2052
|
|
|
1008
2053
|
def render_assistant_message(message)
|
|
1009
|
-
content =
|
|
2054
|
+
content = CLITranscriptFormatter.content_text(message_content(message))
|
|
1010
2055
|
return if content.empty?
|
|
1011
2056
|
|
|
1012
2057
|
render_transcript_block("Assistant", content)
|
|
1013
2058
|
end
|
|
1014
2059
|
|
|
1015
2060
|
def render_tool_message(message, tool_calls_by_id)
|
|
1016
|
-
tool_call = tool_calls_by_id[message_tool_call_id(message)] || synthetic_tool_call(message_name(message), message_tool_call_id(message))
|
|
2061
|
+
tool_call = tool_calls_by_id[message_tool_call_id(message)] || CLITranscriptFormatter.synthetic_tool_call(message_name(message), message_tool_call_id(message))
|
|
1017
2062
|
render_tool_result(tool_call, message_content(message).to_s)
|
|
1018
2063
|
end
|
|
1019
2064
|
|
|
@@ -1106,113 +2151,6 @@ module Kward
|
|
|
1106
2151
|
[["Reasoning", grouped["Reasoning"]], ["Assistant", grouped["Assistant"]]] + others
|
|
1107
2152
|
end
|
|
1108
2153
|
|
|
1109
|
-
def message_reasoning(message)
|
|
1110
|
-
direct = message["reasoning_summary"] || message[:reasoning_summary]
|
|
1111
|
-
return direct.to_s unless direct.to_s.empty?
|
|
1112
|
-
|
|
1113
|
-
content = message_content(message)
|
|
1114
|
-
return "" unless content.is_a?(Array)
|
|
1115
|
-
|
|
1116
|
-
content.filter_map do |part|
|
|
1117
|
-
type = part["type"] || part[:type]
|
|
1118
|
-
next unless ["thinking", "reasoning"].include?(type)
|
|
1119
|
-
|
|
1120
|
-
part["thinking"] || part[:thinking] || part["text"] || part[:text]
|
|
1121
|
-
end.join("\n")
|
|
1122
|
-
end
|
|
1123
|
-
|
|
1124
|
-
def message_content_text(content)
|
|
1125
|
-
case content
|
|
1126
|
-
when Array
|
|
1127
|
-
content.filter_map do |part|
|
|
1128
|
-
type = part["type"] || part[:type]
|
|
1129
|
-
if type == "text"
|
|
1130
|
-
part["text"] || part[:text]
|
|
1131
|
-
elsif type == "image"
|
|
1132
|
-
path = part["path"] || part[:path]
|
|
1133
|
-
media_type = part["media_type"] || part[:media_type] || "image"
|
|
1134
|
-
"[#{media_type}#{path ? ": #{path}" : ""}]"
|
|
1135
|
-
end
|
|
1136
|
-
end.join("\n")
|
|
1137
|
-
else
|
|
1138
|
-
content.to_s
|
|
1139
|
-
end
|
|
1140
|
-
end
|
|
1141
|
-
|
|
1142
|
-
def message_display_text(message)
|
|
1143
|
-
display_content = MessageAccess.display_content(message)
|
|
1144
|
-
return display_content.to_s unless display_content.nil?
|
|
1145
|
-
|
|
1146
|
-
message_content_text(message_content(message))
|
|
1147
|
-
end
|
|
1148
|
-
|
|
1149
|
-
def message_user_display_text(message)
|
|
1150
|
-
display_content = MessageAccess.display_content(message)
|
|
1151
|
-
return display_content.to_s unless display_content.nil?
|
|
1152
|
-
|
|
1153
|
-
content = message_content(message)
|
|
1154
|
-
return content.to_s unless content.is_a?(Array)
|
|
1155
|
-
|
|
1156
|
-
text = content.filter_map do |part|
|
|
1157
|
-
type = part["type"] || part[:type]
|
|
1158
|
-
next unless type == "text"
|
|
1159
|
-
|
|
1160
|
-
part["text"] || part[:text]
|
|
1161
|
-
end.join("\n")
|
|
1162
|
-
Kward::ImageAttachments.display_text_without_references(text, Kward::ImageAttachments.references_from_text(text).select { |reference| reference[:status] == :attached })
|
|
1163
|
-
end
|
|
1164
|
-
|
|
1165
|
-
def message_user_transcript_input(message)
|
|
1166
|
-
content = message_content(message)
|
|
1167
|
-
return content.to_s unless content.is_a?(Array)
|
|
1168
|
-
|
|
1169
|
-
message_user_display_text(message)
|
|
1170
|
-
end
|
|
1171
|
-
|
|
1172
|
-
def message_image_parts(message)
|
|
1173
|
-
content = message_content(message)
|
|
1174
|
-
return [] unless content.is_a?(Array)
|
|
1175
|
-
|
|
1176
|
-
content.select do |part|
|
|
1177
|
-
type = part["type"] || part[:type]
|
|
1178
|
-
type == "image"
|
|
1179
|
-
end
|
|
1180
|
-
end
|
|
1181
|
-
|
|
1182
|
-
def message_image_references(message)
|
|
1183
|
-
message_image_parts(message).map { |part| image_part_reference(part) }
|
|
1184
|
-
end
|
|
1185
|
-
|
|
1186
|
-
def image_part_reference(part)
|
|
1187
|
-
data = part[:data] || part["data"]
|
|
1188
|
-
path = part[:path] || part["path"]
|
|
1189
|
-
media_type = part[:media_type] || part["media_type"] || part[:mimeType] || part["mimeType"] || "image"
|
|
1190
|
-
{
|
|
1191
|
-
status: :attached,
|
|
1192
|
-
type: "image",
|
|
1193
|
-
label: path.to_s.empty? ? "pasted image" : File.basename(path),
|
|
1194
|
-
media_type: media_type,
|
|
1195
|
-
size_bytes: decoded_image_size(data),
|
|
1196
|
-
path: path
|
|
1197
|
-
}
|
|
1198
|
-
end
|
|
1199
|
-
|
|
1200
|
-
def decoded_image_size(data)
|
|
1201
|
-
return nil if data.to_s.empty?
|
|
1202
|
-
|
|
1203
|
-
Base64.decode64(data.to_s.gsub(/\s+/, "")).bytesize
|
|
1204
|
-
rescue ArgumentError
|
|
1205
|
-
nil
|
|
1206
|
-
end
|
|
1207
|
-
|
|
1208
|
-
def synthetic_tool_call(name, id)
|
|
1209
|
-
{
|
|
1210
|
-
"id" => id || "restored_tool",
|
|
1211
|
-
"type" => "function",
|
|
1212
|
-
"function" => { "name" => name || "tool", "arguments" => "{}" }
|
|
1213
|
-
}
|
|
1214
|
-
end
|
|
1215
|
-
|
|
1216
2154
|
def message_role(message)
|
|
1217
2155
|
MessageAccess.role(message)
|
|
1218
2156
|
end
|
|
@@ -1267,10 +2205,7 @@ module Kward
|
|
|
1267
2205
|
end
|
|
1268
2206
|
|
|
1269
2207
|
def select_session_path(session_store)
|
|
1270
|
-
|
|
1271
|
-
sessions = session_store.recent_tree(limit: recent_limit + 1)
|
|
1272
|
-
.reject { |session| active_empty_unnamed_session_info?(session) }
|
|
1273
|
-
.first(recent_limit)
|
|
2208
|
+
sessions = session_store.recent(limit: nil)
|
|
1274
2209
|
if sessions.empty?
|
|
1275
2210
|
@prompt.say("\nNo saved sessions found.\n")
|
|
1276
2211
|
return nil
|
|
@@ -1295,13 +2230,6 @@ module Kward
|
|
|
1295
2230
|
end
|
|
1296
2231
|
end
|
|
1297
2232
|
|
|
1298
|
-
def active_empty_unnamed_session_info?(session)
|
|
1299
|
-
return false unless @active_session
|
|
1300
|
-
return false unless File.expand_path(session.path) == File.expand_path(@active_session.path)
|
|
1301
|
-
|
|
1302
|
-
session.name.to_s.strip.empty? && session.message_count.to_i.zero?
|
|
1303
|
-
end
|
|
1304
|
-
|
|
1305
2233
|
def session_label(session)
|
|
1306
2234
|
title = session.name.to_s.strip
|
|
1307
2235
|
title = session.first_message.to_s.strip if title.empty?
|
|
@@ -1341,6 +2269,7 @@ module Kward
|
|
|
1341
2269
|
prompt_interface = load_prompt_interface
|
|
1342
2270
|
return unless prompt_interface
|
|
1343
2271
|
|
|
2272
|
+
banner_enabled = ConfigFiles.banner_enabled?
|
|
1344
2273
|
@prompt = prompt_interface.new(
|
|
1345
2274
|
slash_commands: slash_command_entries,
|
|
1346
2275
|
overlay_settings: ConfigFiles.overlay_settings,
|
|
@@ -1349,8 +2278,8 @@ module Kward
|
|
|
1349
2278
|
busy_help: ConfigFiles.composer_busy_help?,
|
|
1350
2279
|
attachment_badges: method(:composer_attachment_badges),
|
|
1351
2280
|
attachment_parser: method(:composer_attachment_parser),
|
|
1352
|
-
banner_pixels: Kward::PromptInterface::BANNER_LOGO_PIXELS,
|
|
1353
|
-
banner_message: Kward::PromptInterface::BANNER_MESSAGE
|
|
2281
|
+
banner_pixels: banner_enabled ? Kward::PromptInterface::BANNER_LOGO_PIXELS : nil,
|
|
2282
|
+
banner_message: banner_enabled ? Kward::PromptInterface::BANNER_MESSAGE : nil
|
|
1354
2283
|
)
|
|
1355
2284
|
@prompt.start
|
|
1356
2285
|
end
|
|
@@ -1393,6 +2322,13 @@ module Kward
|
|
|
1393
2322
|
plugin_registry.command_for(command)
|
|
1394
2323
|
end
|
|
1395
2324
|
|
|
2325
|
+
def reload_plugins(conversation)
|
|
2326
|
+
@plugin_registry = PluginRegistry.load(reserved_commands: reserved_slash_command_names)
|
|
2327
|
+
conversation.plugin_registry = @plugin_registry if conversation.respond_to?(:plugin_registry=)
|
|
2328
|
+
conversation.refresh_system_message! if conversation.respond_to?(:refresh_system_message!)
|
|
2329
|
+
@prompt.say("\nPlugins reloaded.\n")
|
|
2330
|
+
end
|
|
2331
|
+
|
|
1396
2332
|
def reserved_slash_command_names
|
|
1397
2333
|
BUILTIN_SLASH_COMMAND_NAMES + prompt_templates.map(&:command)
|
|
1398
2334
|
end
|
|
@@ -1540,6 +2476,18 @@ module Kward
|
|
|
1540
2476
|
"/#{entry[:name]}#{hint}#{description}"
|
|
1541
2477
|
end
|
|
1542
2478
|
|
|
2479
|
+
def auto_name_active_session(input)
|
|
2480
|
+
return unless @active_session
|
|
2481
|
+
return unless @active_session.name.to_s.strip.empty?
|
|
2482
|
+
|
|
2483
|
+
name = default_session_name(input)
|
|
2484
|
+
@active_session.rename(name) unless name.empty?
|
|
2485
|
+
end
|
|
2486
|
+
|
|
2487
|
+
def default_session_name(input)
|
|
2488
|
+
input.to_s.gsub(/\s+/, " ").strip.slice(0, 120).to_s
|
|
2489
|
+
end
|
|
2490
|
+
|
|
1543
2491
|
def run_interactive_turn(agent, input, display_input: nil)
|
|
1544
2492
|
prepare_memory_context(agent.conversation, input) if agent.respond_to?(:conversation)
|
|
1545
2493
|
print_user_transcript(input, display_input: display_input) if prompt_interface?
|
|
@@ -1649,7 +2597,7 @@ module Kward
|
|
|
1649
2597
|
when Events::ToolResult
|
|
1650
2598
|
stream_state[:streamed] = true
|
|
1651
2599
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
1652
|
-
update_session_diff(event.content)
|
|
2600
|
+
update_session_diff(event.content, tool_call: event.tool_call)
|
|
1653
2601
|
print_tool_result(event.tool_call, event.content, line_limit: INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT)
|
|
1654
2602
|
end
|
|
1655
2603
|
end
|
|
@@ -1891,9 +2839,13 @@ module Kward
|
|
|
1891
2839
|
def print_retry(event)
|
|
1892
2840
|
message = retry_message(event)
|
|
1893
2841
|
if prompt_interface?
|
|
1894
|
-
@prompt.
|
|
1895
|
-
|
|
1896
|
-
|
|
2842
|
+
if @prompt.respond_to?(:write_stream_block)
|
|
2843
|
+
@prompt.write_stream_block("Retry", "#{message}\n", finish: true)
|
|
2844
|
+
else
|
|
2845
|
+
@prompt.start_stream_block("Retry")
|
|
2846
|
+
@prompt.write_delta("#{message}\n")
|
|
2847
|
+
@prompt.finish_stream_block
|
|
2848
|
+
end
|
|
1897
2849
|
else
|
|
1898
2850
|
start_stream_block("Retry")
|
|
1899
2851
|
puts message
|
|
@@ -1908,9 +2860,13 @@ module Kward
|
|
|
1908
2860
|
|
|
1909
2861
|
def print_tool_call(tool_call)
|
|
1910
2862
|
if prompt_interface?
|
|
1911
|
-
@prompt.
|
|
1912
|
-
|
|
1913
|
-
|
|
2863
|
+
if @prompt.respond_to?(:write_stream_block)
|
|
2864
|
+
@prompt.write_stream_block("Tool", "#{tool_command(tool_call)}\n", finish: true)
|
|
2865
|
+
else
|
|
2866
|
+
@prompt.start_stream_block("Tool")
|
|
2867
|
+
@prompt.write_delta("#{tool_command(tool_call)}\n")
|
|
2868
|
+
@prompt.finish_stream_block
|
|
2869
|
+
end
|
|
1914
2870
|
else
|
|
1915
2871
|
start_stream_block("Tool")
|
|
1916
2872
|
puts tool_command(tool_call)
|
|
@@ -1923,10 +2879,14 @@ module Kward
|
|
|
1923
2879
|
summary = tool_result_summary(tool_call, content)
|
|
1924
2880
|
summary = limit_tool_output_lines(summary, line_limit) if line_limit
|
|
1925
2881
|
if prompt_interface?
|
|
1926
|
-
|
|
1927
|
-
@prompt.
|
|
1928
|
-
|
|
1929
|
-
|
|
2882
|
+
summary = summary.end_with?("\n") ? summary : "#{summary}\n"
|
|
2883
|
+
if @prompt.respond_to?(:write_stream_block)
|
|
2884
|
+
@prompt.write_stream_block("Tool output", summary, finish: true)
|
|
2885
|
+
else
|
|
2886
|
+
@prompt.start_stream_block("Tool output")
|
|
2887
|
+
@prompt.write_delta(summary)
|
|
2888
|
+
@prompt.finish_stream_block
|
|
2889
|
+
end
|
|
1930
2890
|
else
|
|
1931
2891
|
start_stream_block("Tool output")
|
|
1932
2892
|
print summary
|