kward 0.83.0 → 0.84.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 +68 -15
- data/CONTRIBUTING.md +74 -0
- data/Gemfile.lock +8 -2
- data/README.md +21 -1
- data/Rakefile +46 -2
- data/SECURITY.md +31 -0
- data/doc/api.md +4 -0
- data/doc/composer.md +1 -1
- data/doc/configuration.md +68 -21
- data/doc/editor.md +28 -13
- data/doc/files.md +8 -4
- data/doc/getting-started.md +3 -0
- data/doc/pan.md +19 -15
- data/doc/platform-support.md +48 -0
- data/doc/security.md +3 -2
- data/doc/shell.md +62 -45
- data/doc/troubleshooting.md +12 -2
- data/doc/usage.md +5 -5
- data/kward.gemspec +5 -4
- data/lib/kward/agent.rb +6 -3
- data/lib/kward/ansi.rb +110 -10
- data/lib/kward/cli/auth_commands.rb +34 -13
- data/lib/kward/cli/commands.rb +83 -62
- data/lib/kward/cli/doctor.rb +39 -17
- data/lib/kward/cli/hook_commands.rb +22 -12
- data/lib/kward/cli/interactive_turn.rb +48 -7
- data/lib/kward/cli/project_skills_commands.rb +8 -4
- data/lib/kward/cli/prompt_interface.rb +27 -0
- data/lib/kward/cli/rendering.rb +15 -9
- data/lib/kward/cli/runtime_helpers.rb +118 -55
- data/lib/kward/cli/slash_commands.rb +12 -14
- data/lib/kward/cli/tabs.rb +83 -12
- data/lib/kward/cli/tool_summaries.rb +14 -0
- data/lib/kward/cli.rb +45 -7
- data/lib/kward/cli_transcript_formatter.rb +11 -4
- data/lib/kward/config_files.rb +82 -68
- data/lib/kward/detached_run.rb +44 -0
- data/lib/kward/interactive_pty_runner.rb +102 -28
- data/lib/kward/{ekwsh.rb → kwsh.rb} +35 -16
- data/lib/kward/kwshrc.rb +233 -0
- data/lib/kward/markdown_code_block.rb +136 -0
- data/lib/kward/model/client.rb +34 -22
- data/lib/kward/model/provider_catalog.rb +5 -0
- data/lib/kward/model/stream_parser.rb +20 -4
- data/lib/kward/pan/index.html.erb +3 -3
- data/lib/kward/pan/server.rb +23 -3
- data/lib/kward/persistent_shell_session.rb +119 -26
- data/lib/kward/project_files.rb +2 -2
- data/lib/kward/prompt_interface/composer_renderer.rb +44 -40
- data/lib/kward/prompt_interface/composer_state.rb +33 -24
- data/lib/kward/prompt_interface/editor/auto_indent.rb +24 -22
- data/lib/kward/prompt_interface/editor/controller.rb +30 -33
- data/lib/kward/prompt_interface/editor/endwise.rb +13 -4
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +289 -44
- data/lib/kward/prompt_interface/editor/renderer.rb +108 -6
- data/lib/kward/prompt_interface/editor/runner.rb +362 -0
- data/lib/kward/prompt_interface/editor/runner_state.rb +78 -0
- data/lib/kward/prompt_interface/editor/state.rb +10 -10
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +68 -6
- data/lib/kward/prompt_interface/editor/vibe_state.rb +3 -3
- data/lib/kward/prompt_interface/file_overlay.rb +71 -15
- data/lib/kward/prompt_interface/key_handler.rb +67 -0
- data/lib/kward/prompt_interface/overlay_renderer.rb +7 -5
- data/lib/kward/prompt_interface/project_browser.rb +415 -14
- data/lib/kward/prompt_interface/runtime_state.rb +50 -1
- data/lib/kward/prompt_interface/screen.rb +2 -2
- data/lib/kward/prompt_interface/selection_prompt.rb +3 -1
- data/lib/kward/prompt_interface/slash_overlay.rb +19 -4
- data/lib/kward/prompt_interface/transcript_renderer.rb +12 -7
- data/lib/kward/prompt_interface.rb +93 -18
- data/lib/kward/prompts/commands.rb +1 -1
- data/lib/kward/prompts.rb +1 -1
- data/lib/kward/pty_output_sink.rb +47 -0
- data/lib/kward/rpc/transcript_normalizer.rb +7 -3
- data/lib/kward/scratchpad_languages.rb +74 -0
- data/lib/kward/scratchpad_runner.rb +155 -29
- data/lib/kward/shell_prompt.rb +2 -0
- data/lib/kward/terminal_keys.rb +12 -0
- data/lib/kward/terminal_text.rb +121 -0
- data/lib/kward/text_matcher.rb +18 -0
- data/lib/kward/tools/open_editor.rb +41 -0
- data/lib/kward/tools/registry.rb +13 -4
- data/lib/kward/tools/tool_call.rb +2 -1
- data/lib/kward/version.rb +1 -1
- data/templates/default/fulldoc/html/css/kward.css +0 -125
- data/templates/default/fulldoc/html/images/kward_workflow.svg +52 -0
- data/templates/default/fulldoc/html/setup.rb +1 -1
- data/templates/default/kward_navigation.rb +1 -0
- data/templates/default/layout/html/footer.erb +10 -0
- data/templates/default/layout/html/headers.erb +23 -0
- data/templates/default/layout/html/layout.erb +6 -18
- data/templates/default/layout/html/setup.rb +41 -2
- metadata +36 -8
- data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
|
@@ -47,9 +47,10 @@ module Kward
|
|
|
47
47
|
def build_editor_prompt_agent(active_agent, context, registry)
|
|
48
48
|
active_conversation = active_agent.conversation
|
|
49
49
|
config = safely_read_config.to_h
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
configured_provider = ConfigFiles.editor_agent_provider(config)
|
|
51
|
+
provider = configured_provider || active_conversation.provider || current_model_provider
|
|
52
|
+
model = ConfigFiles.editor_agent_model(config) || (configured_provider ? nil : active_conversation.model || current_model_id)
|
|
53
|
+
reasoning = ConfigFiles.editor_agent_reasoning_effort(config) || (configured_provider ? nil : active_conversation.reasoning_effort || current_reasoning_effort)
|
|
53
54
|
conversation = Conversation.new(
|
|
54
55
|
system_message: EditorPrompt.system_message,
|
|
55
56
|
workspace_root: context[:workspace_root] || active_conversation.workspace_root,
|
|
@@ -61,6 +62,7 @@ module Kward
|
|
|
61
62
|
client: @client,
|
|
62
63
|
tool_registry: registry,
|
|
63
64
|
conversation: conversation,
|
|
65
|
+
strict_provider: !configured_provider.nil?,
|
|
64
66
|
warning_sink: ConfigFiles.warning_sink
|
|
65
67
|
)
|
|
66
68
|
end
|
|
@@ -76,9 +78,11 @@ module Kward
|
|
|
76
78
|
event_queue = Queue.new
|
|
77
79
|
stream_state = {
|
|
78
80
|
streamed: false,
|
|
81
|
+
assistant_arrived: false,
|
|
79
82
|
last_flush: monotonic_now,
|
|
80
83
|
stream_block_open: false,
|
|
81
84
|
markdown_streams: {},
|
|
85
|
+
tool_elapsed_ms: {},
|
|
82
86
|
defer_assistant_streaming: defer_assistant_streaming?(agent)
|
|
83
87
|
}
|
|
84
88
|
markdown_chunks = []
|
|
@@ -126,19 +130,29 @@ module Kward
|
|
|
126
130
|
if busy_replacement_agent? || suppress_transcript
|
|
127
131
|
discard_interactive_events(event_queue, markdown_chunks, stream_state, force: true)
|
|
128
132
|
else
|
|
129
|
-
|
|
133
|
+
drain_remaining_interactive_events(event_queue, markdown_chunks, stream_state, agent)
|
|
130
134
|
end
|
|
131
135
|
completed = !cancelled && !busy_replacement_agent? && !error
|
|
132
136
|
on_complete&.call(successful: completed, cancelled: cancelled, error: error, answer: answer)
|
|
133
137
|
raise error if error && !error.is_a?(Cancellation::CancelledError) && !busy_replacement_agent?
|
|
134
138
|
|
|
135
139
|
@prompt.commit_editor_prompt(editor_prompt_session) if editor_prompt_session && completed
|
|
136
|
-
@prompt.say("\n#{colored(
|
|
140
|
+
@prompt.say("\n#{colored(transcript_marker("Assistant"), :activity, :bold)} #{render_markdown_transcript(answer)}\n") unless suppress_transcript || cancelled || busy_replacement_agent? || stream_state[:streamed] || answer.to_s.empty?
|
|
137
141
|
persist_memory_state(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
138
142
|
auto_summarize_memory(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript && queued_inputs.empty? && !cancelled
|
|
139
143
|
queued_inputs
|
|
140
144
|
ensure
|
|
141
145
|
@prompt.finish_busy_input if @prompt.respond_to?(:finish_busy_input)
|
|
146
|
+
if @prompt.respond_to?(:show_completion) && !busy_replacement_agent?
|
|
147
|
+
completion = if cancelled
|
|
148
|
+
:cancelled
|
|
149
|
+
elsif error
|
|
150
|
+
:failed
|
|
151
|
+
elsif completed
|
|
152
|
+
:success
|
|
153
|
+
end
|
|
154
|
+
@prompt.show_completion(completion) if completion
|
|
155
|
+
end
|
|
142
156
|
if editor_prompt_session && @prompt.respond_to?(:resume_editor_for_agent)
|
|
143
157
|
status = if cancelled
|
|
144
158
|
"Agent request cancelled"
|
|
@@ -174,6 +188,14 @@ module Kward
|
|
|
174
188
|
flush_interactive_markdown_deltas(markdown_chunks, stream_state, force: force)
|
|
175
189
|
end
|
|
176
190
|
|
|
191
|
+
def drain_remaining_interactive_events(event_queue, markdown_chunks, stream_state, agent)
|
|
192
|
+
until event_queue.empty?
|
|
193
|
+
drain_interactive_events(event_queue, markdown_chunks, stream_state, agent)
|
|
194
|
+
sleep STREAM_RENDER_INTERVAL unless event_queue.empty?
|
|
195
|
+
end
|
|
196
|
+
drain_interactive_events(event_queue, markdown_chunks, stream_state, agent, force: true)
|
|
197
|
+
end
|
|
198
|
+
|
|
177
199
|
def discard_interactive_events(event_queue, markdown_chunks, stream_state, force: false)
|
|
178
200
|
drained = 0
|
|
179
201
|
loop do
|
|
@@ -193,12 +215,19 @@ module Kward
|
|
|
193
215
|
case event
|
|
194
216
|
when Events::ReasoningDelta
|
|
195
217
|
stream_state[:streamed] = true
|
|
218
|
+
update_busy_activity("reasoning")
|
|
196
219
|
append_markdown_delta(markdown_chunks, "Reasoning", event.delta)
|
|
197
220
|
when Events::ReasoningBoundary
|
|
198
221
|
stream_state[:streamed] = true
|
|
199
222
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
223
|
+
update_busy_activity("thinking")
|
|
200
224
|
when Events::AssistantDelta
|
|
201
225
|
stream_state[:streamed] = true
|
|
226
|
+
unless stream_state[:assistant_arrived]
|
|
227
|
+
@prompt.show_response_arrival if @prompt.respond_to?(:show_response_arrival)
|
|
228
|
+
stream_state[:assistant_arrived] = true
|
|
229
|
+
end
|
|
230
|
+
update_busy_activity("responding")
|
|
202
231
|
append_markdown_delta(markdown_chunks, "Assistant", event.delta)
|
|
203
232
|
when Events::Steering
|
|
204
233
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
@@ -208,18 +237,30 @@ module Kward
|
|
|
208
237
|
when Events::Retry
|
|
209
238
|
stream_state[:streamed] = true
|
|
210
239
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
240
|
+
update_busy_activity("retrying")
|
|
211
241
|
print_retry(event)
|
|
212
242
|
when Events::ToolCall
|
|
213
243
|
stream_state[:streamed] = true
|
|
214
244
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
245
|
+
update_busy_activity(tool_activity(event.tool_call))
|
|
246
|
+
when Events::ToolUpdate
|
|
247
|
+
key = tool_call_id(event.tool_call) || event.tool_call.object_id
|
|
248
|
+
stream_state[:tool_elapsed_ms][key] = event.elapsed_ms
|
|
215
249
|
when Events::ToolResult
|
|
216
250
|
stream_state[:streamed] = true
|
|
217
251
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
218
252
|
update_session_diff(event.content, tool_call: event.tool_call)
|
|
219
|
-
|
|
253
|
+
key = tool_call_id(event.tool_call) || event.tool_call.object_id
|
|
254
|
+
elapsed_ms = stream_state[:tool_elapsed_ms].delete(key)
|
|
255
|
+
print_tool_result(event.tool_call, event.content, line_limit: INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT, elapsed_ms: elapsed_ms)
|
|
256
|
+
update_busy_activity("thinking")
|
|
220
257
|
end
|
|
221
258
|
end
|
|
222
259
|
|
|
260
|
+
def update_busy_activity(activity)
|
|
261
|
+
@prompt.update_busy_activity(activity) if @prompt.respond_to?(:update_busy_activity)
|
|
262
|
+
end
|
|
263
|
+
|
|
223
264
|
def flush_interactive_markdown_deltas(markdown_chunks, stream_state, force: false)
|
|
224
265
|
if force
|
|
225
266
|
finish_interactive_markdown_deltas(markdown_chunks, stream_state)
|
|
@@ -347,7 +388,7 @@ module Kward
|
|
|
347
388
|
streamed = true if !suppress_transcript && render_blocking_turn_event(event, markdown_chunks, tool_line_limit: INTERACTIVE_TOOL_OUTPUT_LINE_LIMIT)
|
|
348
389
|
end
|
|
349
390
|
flush_markdown_deltas(markdown_chunks) if streamed
|
|
350
|
-
@prompt.say("\n#{colored(
|
|
391
|
+
@prompt.say("\n#{colored(transcript_marker("Assistant"), :activity, :bold)} #{render_markdown_transcript(answer)}\n") unless suppress_transcript || streamed || answer.to_s.empty?
|
|
351
392
|
persist_memory_state(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
352
393
|
auto_summarize_memory(agent.conversation) if agent.respond_to?(:conversation) && !suppress_transcript
|
|
353
394
|
on_complete&.call(successful: true, cancelled: false, error: nil, answer: answer)
|
|
@@ -7,20 +7,20 @@ module Kward
|
|
|
7
7
|
argument = Array(arguments).join(" ").strip.downcase
|
|
8
8
|
_workspace_root, candidates, coordinator = project_skill_trust_coordinator
|
|
9
9
|
if candidates.empty? && argument != "untrust"
|
|
10
|
-
|
|
10
|
+
project_skills_cli_output("No project skills found in the current workspace.")
|
|
11
11
|
return
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
case argument
|
|
15
15
|
when "", "status"
|
|
16
16
|
lines = candidates.empty? ? ["No project skills found in the current workspace."] : candidates.map { |candidate| "#{relative_workspace_path(candidate.path)}: #{coordinator.decision(candidate) || "needs review"}" }
|
|
17
|
-
|
|
17
|
+
project_skills_cli_output(lines.join("\n"))
|
|
18
18
|
when "trust"
|
|
19
19
|
coordinator.record!(candidates, "allow")
|
|
20
|
-
|
|
20
|
+
project_skills_cli_output("Project skills trusted for the current skill snapshots.")
|
|
21
21
|
when "untrust"
|
|
22
22
|
coordinator.remove_workspace!
|
|
23
|
-
|
|
23
|
+
project_skills_cli_output("Project skill trust removed for this workspace.")
|
|
24
24
|
when "review"
|
|
25
25
|
review_project_skills(candidates)
|
|
26
26
|
else
|
|
@@ -43,6 +43,10 @@ module Kward
|
|
|
43
43
|
|
|
44
44
|
private
|
|
45
45
|
|
|
46
|
+
def project_skills_cli_output(message)
|
|
47
|
+
@prompt.say("#{colored("Project skills", :green, :bold)}\n\n#{message}")
|
|
48
|
+
end
|
|
49
|
+
|
|
46
50
|
def project_skill_trust_coordinator
|
|
47
51
|
workspace_root = current_workspace_root
|
|
48
52
|
candidates = ConfigFiles.project_skill_candidates(workspace_root: workspace_root)
|
|
@@ -45,6 +45,7 @@ module Kward
|
|
|
45
45
|
editor_line_numbers_source: -> { ConfigFiles.editor_line_numbers },
|
|
46
46
|
diff_view: ConfigFiles.diff_view,
|
|
47
47
|
diff_view_source: -> { ConfigFiles.diff_view },
|
|
48
|
+
editor_runners_source: -> { ConfigFiles.editor_runners },
|
|
48
49
|
redraw_handler: method(:redraw_interactive_prompt)
|
|
49
50
|
)
|
|
50
51
|
if @prompt.method(:start).parameters.any? { |kind, name| [:key, :keyreq].include?(kind) && name == :render }
|
|
@@ -99,6 +100,13 @@ module Kward
|
|
|
99
100
|
@prompt.respond_to?(:start_stream_block) && @prompt.respond_to?(:write_delta)
|
|
100
101
|
end
|
|
101
102
|
|
|
103
|
+
def open_editor_for_agent(path, base_dir:, allow_new:)
|
|
104
|
+
return false unless prompt_interface?
|
|
105
|
+
return false unless @prompt.respond_to?(:edit_file)
|
|
106
|
+
|
|
107
|
+
@prompt.edit_file(path, base_dir: base_dir, allow_new: allow_new)
|
|
108
|
+
end
|
|
109
|
+
|
|
102
110
|
def emit_warning(message)
|
|
103
111
|
sink = ConfigFiles.warning_sink
|
|
104
112
|
sink ? sink.call(message) : warn(message)
|
|
@@ -129,6 +137,7 @@ module Kward
|
|
|
129
137
|
[
|
|
130
138
|
startup_status_line(refresh_update_check: refresh_update_check),
|
|
131
139
|
*startup_update_notice_lines,
|
|
140
|
+
*startup_authentication_notice_lines,
|
|
132
141
|
"",
|
|
133
142
|
startup_info_line("Workspace", startup_workspace_label),
|
|
134
143
|
startup_info_line("Branch", startup_branch_value),
|
|
@@ -198,6 +207,24 @@ module Kward
|
|
|
198
207
|
@startup_update_notice = @startup_update_check.notice(refresh: refresh)
|
|
199
208
|
end
|
|
200
209
|
|
|
210
|
+
def startup_authentication_notice_lines
|
|
211
|
+
return [] unless startup_authentication_required?
|
|
212
|
+
|
|
213
|
+
[
|
|
214
|
+
ANSI.colorize(" No model provider is connected.", :yellow, enabled: @color_enabled),
|
|
215
|
+
" Run /login to sign in, or /model to configure a local server."
|
|
216
|
+
]
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def startup_authentication_required?
|
|
220
|
+
return false unless @client.is_a?(Client)
|
|
221
|
+
return false if @client.current_provider == "Local"
|
|
222
|
+
|
|
223
|
+
auth_credentials.none? { |credential| credential.fetch(:configured) }
|
|
224
|
+
rescue StandardError
|
|
225
|
+
false
|
|
226
|
+
end
|
|
227
|
+
|
|
201
228
|
def startup_info_line(label, value)
|
|
202
229
|
"#{ANSI.colorize(label.ljust(12), :gray, enabled: @color_enabled)}#{ANSI.colorize(value, :cyan, enabled: @color_enabled)}"
|
|
203
230
|
end
|
data/lib/kward/cli/rendering.rb
CHANGED
|
@@ -223,7 +223,7 @@ module Kward
|
|
|
223
223
|
# Writes the user transcript output for the terminal CLI flow.
|
|
224
224
|
def print_user_transcript(input, display_input: nil, attachment_references: nil, image_parts: nil)
|
|
225
225
|
visible_input = display_input.nil? ? input : display_input
|
|
226
|
-
write_prompt_transcript("\n#{colored("You>", :blue, :bold)} #{visible_input}\n")
|
|
226
|
+
write_prompt_transcript("\n#{colored("❯ You>", :blue, :bold)} #{visible_input}\n")
|
|
227
227
|
print_attachment_badges(input, references: attachment_references)
|
|
228
228
|
print_pasted_images(input, image_parts: image_parts)
|
|
229
229
|
end
|
|
@@ -347,8 +347,9 @@ module Kward
|
|
|
347
347
|
end
|
|
348
348
|
|
|
349
349
|
# Writes the tool result output for the terminal CLI flow.
|
|
350
|
-
def print_tool_result(tool_call, content, line_limit: nil)
|
|
350
|
+
def print_tool_result(tool_call, content, line_limit: nil, elapsed_ms: nil)
|
|
351
351
|
summary = tool_result_summary(tool_call, content)
|
|
352
|
+
summary = tool_summary_with_duration(summary, elapsed_ms)
|
|
352
353
|
summary = limit_tool_output_lines(summary, line_limit) if line_limit
|
|
353
354
|
display_summary = tool_summary_display_text(summary)
|
|
354
355
|
if prompt_interface?
|
|
@@ -380,7 +381,7 @@ module Kward
|
|
|
380
381
|
return if @stream_block == label
|
|
381
382
|
|
|
382
383
|
puts if @stream_block
|
|
383
|
-
print "\n#{colored(
|
|
384
|
+
print "\n#{colored(transcript_marker(label), *label_styles(label))} "
|
|
384
385
|
@stream_block = label
|
|
385
386
|
end
|
|
386
387
|
|
|
@@ -408,20 +409,25 @@ module Kward
|
|
|
408
409
|
end
|
|
409
410
|
end
|
|
410
411
|
|
|
412
|
+
def transcript_marker(label)
|
|
413
|
+
marker = "#{transcript_label(label)}>"
|
|
414
|
+
label == "Assistant" ? "✦ #{marker}" : marker
|
|
415
|
+
end
|
|
416
|
+
|
|
411
417
|
def label_styles(label)
|
|
412
418
|
case label
|
|
413
419
|
when "Reasoning", "Compaction summary"
|
|
414
|
-
[:
|
|
420
|
+
[:metadata, :bold]
|
|
415
421
|
when "Assistant", "Kward"
|
|
416
|
-
[:
|
|
422
|
+
[:activity, :bold]
|
|
417
423
|
when "Tool", "Tool output"
|
|
418
|
-
[:
|
|
424
|
+
[:tool, :bold]
|
|
419
425
|
when "Tool failed"
|
|
420
|
-
[:
|
|
426
|
+
[:failure, :bold]
|
|
421
427
|
when "Retry"
|
|
422
|
-
[:
|
|
428
|
+
[:caution, :bold]
|
|
423
429
|
else
|
|
424
|
-
[:
|
|
430
|
+
[:metadata, :bold]
|
|
425
431
|
end
|
|
426
432
|
end
|
|
427
433
|
|