kward 0.80.0 → 0.81.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 +40 -0
- data/Gemfile.lock +2 -2
- data/README.md +1 -1
- data/doc/composer.md +5 -8
- data/doc/configuration.md +13 -3
- data/doc/permissions.md +1 -1
- data/doc/rpc.md +2 -1
- data/doc/sandboxing.md +4 -4
- data/doc/security.md +6 -9
- data/doc/shell.md +161 -196
- data/doc/skills.md +19 -5
- data/doc/tabs.md +1 -1
- data/doc/usage.md +6 -5
- data/lib/kward/agent.rb +11 -6
- data/lib/kward/ansi.rb +9 -1
- data/lib/kward/cli/commands.rb +7 -0
- data/lib/kward/cli/interactive_turn.rb +1 -1
- data/lib/kward/cli/memory_commands.rb +2 -2
- data/lib/kward/cli/plugins.rb +1 -1
- data/lib/kward/cli/project_skills.rb +99 -0
- data/lib/kward/cli/project_skills_commands.rb +87 -0
- data/lib/kward/cli/prompt_interface.rb +54 -4
- data/lib/kward/cli/rendering.rb +28 -2
- data/lib/kward/cli/runtime_helpers.rb +157 -19
- data/lib/kward/cli/sessions.rb +6 -4
- data/lib/kward/cli/settings.rb +1 -1
- data/lib/kward/cli/slash_commands.rb +7 -1
- data/lib/kward/cli/tabs.rb +4 -1
- data/lib/kward/cli.rb +21 -1
- data/lib/kward/compactor.rb +7 -2
- data/lib/kward/config_files.rb +30 -9
- data/lib/kward/conversation.rb +8 -5
- data/lib/kward/ekwsh.rb +37 -16
- data/lib/kward/hooks/audit_log.rb +5 -2
- data/lib/kward/interactive_pty_runner.rb +88 -22
- data/lib/kward/plugin_registry.rb +20 -13
- data/lib/kward/prompt_interface/composer_controller.rb +13 -1
- data/lib/kward/prompt_interface/editor/controller.rb +4 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +35 -10
- data/lib/kward/prompt_interface/key_handler.rb +78 -43
- data/lib/kward/prompt_interface/overlay_renderer.rb +12 -0
- data/lib/kward/prompt_interface/screen.rb +5 -0
- data/lib/kward/prompt_interface.rb +93 -21
- data/lib/kward/prompts/commands.rb +2 -0
- data/lib/kward/prompts/templates.rb +11 -6
- data/lib/kward/prompts.rb +6 -6
- data/lib/kward/rpc/server.rb +1 -0
- data/lib/kward/session_store.rb +3 -2
- data/lib/kward/skills/registry.rb +67 -12
- data/lib/kward/skills/trust_coordinator.rb +45 -0
- data/lib/kward/skills/trust_store.rb +107 -0
- data/lib/kward/telemetry/logger.rb +5 -2
- data/lib/kward/version.rb +1 -1
- metadata +5 -1
data/lib/kward/ansi.rb
CHANGED
|
@@ -67,7 +67,7 @@ module Kward
|
|
|
67
67
|
|
|
68
68
|
# Drops unsafe terminal controls from transcript text while preserving SGR color.
|
|
69
69
|
def sanitize_transcript(text)
|
|
70
|
-
scan_escape_tokens(text).each_with_object(+"") do |token, sanitized|
|
|
70
|
+
scan_escape_tokens(normalize_transcript_encoding(text)).each_with_object(+"") do |token, sanitized|
|
|
71
71
|
if token[:escape]
|
|
72
72
|
sanitized << token[:text] if token[:text].match?(SGR_PATTERN)
|
|
73
73
|
else
|
|
@@ -76,6 +76,14 @@ module Kward
|
|
|
76
76
|
end
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
+
def normalize_transcript_encoding(text)
|
|
80
|
+
string = text.to_s.dup
|
|
81
|
+
return string unless string.encoding == Encoding::ASCII_8BIT
|
|
82
|
+
|
|
83
|
+
string.force_encoding(Encoding::UTF_8)
|
|
84
|
+
string.valid_encoding? ? string : string.scrub
|
|
85
|
+
end
|
|
86
|
+
|
|
79
87
|
def wrap_visible(text, width)
|
|
80
88
|
line_width = [width.to_i, 1].max
|
|
81
89
|
rows = []
|
data/lib/kward/cli/commands.rb
CHANGED
|
@@ -54,6 +54,7 @@ module Kward
|
|
|
54
54
|
#{command.call("kward init")} Install starter prompts and PRINCIPLES.md
|
|
55
55
|
#{command.call("kward doctor")} Check local Kward setup
|
|
56
56
|
#{command.call("kward hooks doctor")} Inspect lifecycle hook setup
|
|
57
|
+
#{command.call("kward skills status")} Inspect project skill trust
|
|
57
58
|
#{command.call("kward edit")} #{option.call("<filename>")} Open a file in the integrated editor
|
|
58
59
|
#{command.call("kward sysprompt")} Inspect the effective system prompt
|
|
59
60
|
#{command.call("kward openrouter refresh")} Refresh cached OpenRouter models
|
|
@@ -69,6 +70,7 @@ module Kward
|
|
|
69
70
|
#{command.call("init")} Install starter prompts and PRINCIPLES.md
|
|
70
71
|
#{command.call("doctor")} Check local Kward setup
|
|
71
72
|
#{command.call("hooks list|events|logs|doctor|trust|untrust")} Inspect lifecycle hooks
|
|
73
|
+
#{command.call("skills status|trust|untrust|review")} Manage project skill trust
|
|
72
74
|
#{command.call("edit")} #{option.call("<filename>")} Open a file in the integrated editor
|
|
73
75
|
#{command.call("sysprompt")} [--raw] Inspect the effective system prompt
|
|
74
76
|
#{command.call("stats tokens")} [range] [options] Export local token telemetry as CSV
|
|
@@ -136,6 +138,11 @@ module Kward
|
|
|
136
138
|
description: "Inspect lifecycle hooks, recent audit records, diagnostics, and workspace hook trust.",
|
|
137
139
|
examples: ["kward hooks list", "kward hooks doctor", "kward hooks logs 50", "kward hooks trust"]
|
|
138
140
|
},
|
|
141
|
+
"skills" => {
|
|
142
|
+
usage: "kward skills [status|trust|untrust|review]",
|
|
143
|
+
description: "Inspect or manage project skill trust for the current workspace.",
|
|
144
|
+
examples: ["kward skills status", "kward skills review", "kward skills trust"]
|
|
145
|
+
},
|
|
139
146
|
"edit" => {
|
|
140
147
|
usage: "kward edit <filename>",
|
|
141
148
|
description: "Open a file in the integrated editor.",
|
|
@@ -8,7 +8,6 @@ module Kward
|
|
|
8
8
|
|
|
9
9
|
def run_interactive_turn(agent, input, display_input: nil)
|
|
10
10
|
prepare_memory_context(agent.conversation, input) if agent.respond_to?(:conversation)
|
|
11
|
-
print_user_transcript(input, display_input: display_input) if prompt_interface?
|
|
12
11
|
return run_blocking_interactive_turn(agent, input, display_input: display_input) unless prompt_interface?
|
|
13
12
|
|
|
14
13
|
queued_inputs = []
|
|
@@ -27,6 +26,7 @@ module Kward
|
|
|
27
26
|
answer = nil
|
|
28
27
|
error = nil
|
|
29
28
|
@prompt.begin_busy_input("You>") if @prompt.respond_to?(:begin_busy_input)
|
|
29
|
+
print_user_transcript(input, display_input: display_input)
|
|
30
30
|
|
|
31
31
|
worker = Thread.new do
|
|
32
32
|
options = agent_display_options(display_input)
|
|
@@ -104,7 +104,7 @@ module Kward
|
|
|
104
104
|
def prepare_memory_context(conversation, input)
|
|
105
105
|
Memory::TurnContext.apply(conversation: conversation, input: input)
|
|
106
106
|
rescue StandardError => e
|
|
107
|
-
|
|
107
|
+
emit_warning "Memory retrieval failed: #{e.message}"
|
|
108
108
|
nil
|
|
109
109
|
end
|
|
110
110
|
|
|
@@ -120,7 +120,7 @@ module Kward
|
|
|
120
120
|
|
|
121
121
|
summarize_memory(conversation, manager: manager)
|
|
122
122
|
rescue StandardError => e
|
|
123
|
-
|
|
123
|
+
emit_warning "Memory auto-summary failed: #{e.message}"
|
|
124
124
|
nil
|
|
125
125
|
end
|
|
126
126
|
|
data/lib/kward/cli/plugins.rb
CHANGED
|
@@ -122,7 +122,7 @@ module Kward
|
|
|
122
122
|
argument_hint: template.argument_hint
|
|
123
123
|
}
|
|
124
124
|
end
|
|
125
|
-
skill_entries = ConfigFiles.skills.map do |skill|
|
|
125
|
+
skill_entries = ConfigFiles.skills(workspace_root: current_workspace_root, project_skill_paths: @interactive_project_skill_paths || []).map do |skill|
|
|
126
126
|
{
|
|
127
127
|
name: "skill:#{skill.name}",
|
|
128
128
|
description: skill.description,
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
require "pathname"
|
|
2
|
+
require_relative "../skills/trust_coordinator"
|
|
3
|
+
require_relative "../skills/trust_store"
|
|
4
|
+
|
|
5
|
+
# Namespace for CLI orchestration helpers.
|
|
6
|
+
module Kward
|
|
7
|
+
class CLI
|
|
8
|
+
# Coordinates interactive trust decisions for project-provided Agent Skills.
|
|
9
|
+
module ProjectSkills
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def prepare_interactive_project_skills
|
|
13
|
+
return unless prompt_interface?
|
|
14
|
+
|
|
15
|
+
workspace_root = current_workspace_root
|
|
16
|
+
candidates = ConfigFiles.project_skill_candidates(workspace_root: workspace_root)
|
|
17
|
+
trust_store = Skills::TrustStore.new(config_dir: ConfigFiles.config_dir)
|
|
18
|
+
coordinator = Skills::TrustCoordinator.new(workspace_root: workspace_root, trust_store: trust_store)
|
|
19
|
+
|
|
20
|
+
if ConfigFiles.project_skills_trusted?
|
|
21
|
+
@interactive_project_skill_paths = candidates.map(&:path)
|
|
22
|
+
else
|
|
23
|
+
pending = coordinator.pending(candidates)
|
|
24
|
+
unless pending.empty?
|
|
25
|
+
decision = prompt_for_project_skill_trust(pending)
|
|
26
|
+
coordinator.record!(pending, decision)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
@interactive_project_skill_paths = coordinator.allowed_paths(candidates)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def prompt_for_project_skill_trust(candidates)
|
|
34
|
+
paths = candidates.map { |candidate| " #{relative_workspace_path(candidate.path)}" }
|
|
35
|
+
message = (["Project skills found in #{current_workspace_root}:", *paths, "", "These files contain instructions that may influence the model."]).join("\n")
|
|
36
|
+
loop do
|
|
37
|
+
choice = @prompt.select(message, ["Allow", "Deny", "Review"], title: "Trust project skills")
|
|
38
|
+
case choice.to_s.downcase
|
|
39
|
+
when "review"
|
|
40
|
+
review_project_skills(candidates)
|
|
41
|
+
when "allow"
|
|
42
|
+
return "allow"
|
|
43
|
+
else
|
|
44
|
+
return "deny"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def review_project_skills(candidates)
|
|
50
|
+
candidates.each do |candidate|
|
|
51
|
+
content = read_project_skill_for_review(candidate.path)
|
|
52
|
+
resources = project_skill_resources(candidate.path)
|
|
53
|
+
details = [
|
|
54
|
+
"Project skill: #{relative_workspace_path(candidate.path)}",
|
|
55
|
+
resources.empty? ? nil : "Referenced resources:\n#{resources.map { |path| " #{path}" }.join("\n")}",
|
|
56
|
+
"",
|
|
57
|
+
content
|
|
58
|
+
].compact.join("\n")
|
|
59
|
+
@prompt.say("\n#{ANSI.strip(details)}\n")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def read_project_skill_for_review(path)
|
|
64
|
+
return "Unable to review: file is too large." if File.size(path) > ConfigFiles::MAX_SKILL_FILE_BYTES
|
|
65
|
+
|
|
66
|
+
ANSI.strip(File.read(path, ConfigFiles::MAX_SKILL_FILE_BYTES + 1))
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
"Unable to review: #{e.message}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def project_skill_resources(path)
|
|
72
|
+
folder = File.dirname(path)
|
|
73
|
+
roots = %w[scripts references assets].map { |name| File.join(folder, name) }.select { |root| Dir.exist?(root) }
|
|
74
|
+
roots.flat_map { |root| Dir.glob(File.join(root, "**", "*")).select { |entry| File.file?(entry) } }.sort.first(200).map do |resource|
|
|
75
|
+
Pathname.new(resource).relative_path_from(Pathname.new(folder)).to_s
|
|
76
|
+
end
|
|
77
|
+
rescue StandardError
|
|
78
|
+
[]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def project_skill_paths_for(workspace_root)
|
|
82
|
+
return unless @interactive_project_skill_paths
|
|
83
|
+
return unless canonical_path(workspace_root) == canonical_path(current_workspace_root)
|
|
84
|
+
|
|
85
|
+
@interactive_project_skill_paths
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def canonical_path(path)
|
|
89
|
+
File.realpath(path)
|
|
90
|
+
rescue SystemCallError
|
|
91
|
+
File.expand_path(path)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def relative_workspace_path(path)
|
|
95
|
+
Pathname.new(File.expand_path(path)).relative_path_from(Pathname.new(File.expand_path(current_workspace_root))).to_s
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Namespace for CLI orchestration helpers.
|
|
2
|
+
module Kward
|
|
3
|
+
class CLI
|
|
4
|
+
# Handles explicit project skill trust management commands.
|
|
5
|
+
module ProjectSkillsCommands
|
|
6
|
+
def handle_project_skills_cli_command(arguments)
|
|
7
|
+
argument = Array(arguments).join(" ").strip.downcase
|
|
8
|
+
_workspace_root, candidates, coordinator = project_skill_trust_coordinator
|
|
9
|
+
if candidates.empty? && argument != "untrust"
|
|
10
|
+
@prompt.say("No project skills found in the current workspace.")
|
|
11
|
+
return
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
case argument
|
|
15
|
+
when "", "status"
|
|
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
|
+
@prompt.say(lines.join("\n"))
|
|
18
|
+
when "trust"
|
|
19
|
+
coordinator.record!(candidates, "allow")
|
|
20
|
+
@prompt.say("Project skills trusted for the current skill snapshots.")
|
|
21
|
+
when "untrust"
|
|
22
|
+
coordinator.remove_workspace!
|
|
23
|
+
@prompt.say("Project skill trust removed for this workspace.")
|
|
24
|
+
when "review"
|
|
25
|
+
review_project_skills(candidates)
|
|
26
|
+
else
|
|
27
|
+
raise ArgumentError, "Usage: kward skills [status|trust|untrust|review]"
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def handle_project_skills_command(argument)
|
|
32
|
+
case argument.to_s.strip.downcase
|
|
33
|
+
when "", "status"
|
|
34
|
+
show_project_skills_status
|
|
35
|
+
when "trust"
|
|
36
|
+
trust_current_project_skills
|
|
37
|
+
when "untrust"
|
|
38
|
+
untrust_current_project_skills
|
|
39
|
+
else
|
|
40
|
+
runtime_output("Usage: /skills [status|trust|untrust]")
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def project_skill_trust_coordinator
|
|
47
|
+
workspace_root = current_workspace_root
|
|
48
|
+
candidates = ConfigFiles.project_skill_candidates(workspace_root: workspace_root)
|
|
49
|
+
store = Skills::TrustStore.new(config_dir: ConfigFiles.config_dir)
|
|
50
|
+
[workspace_root, candidates, Skills::TrustCoordinator.new(workspace_root: workspace_root, trust_store: store)]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def show_project_skills_status
|
|
54
|
+
_workspace_root, candidates, coordinator = project_skill_trust_coordinator
|
|
55
|
+
if candidates.empty?
|
|
56
|
+
runtime_output("No project skills found in the current workspace.")
|
|
57
|
+
return
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
lines = candidates.map do |candidate|
|
|
61
|
+
status = coordinator.decision(candidate) || "needs review"
|
|
62
|
+
"#{relative_workspace_path(candidate.path)}: #{status}"
|
|
63
|
+
end
|
|
64
|
+
runtime_output(lines.join("\n"))
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def trust_current_project_skills
|
|
68
|
+
_workspace_root, candidates, coordinator = project_skill_trust_coordinator
|
|
69
|
+
if candidates.empty?
|
|
70
|
+
runtime_output("No project skills found in the current workspace.")
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
coordinator.record!(candidates, "allow")
|
|
75
|
+
@interactive_project_skill_paths = coordinator.allowed_paths(candidates)
|
|
76
|
+
runtime_output("Project skills trusted for the current skill snapshots. Use /new to rebuild the agent.")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def untrust_current_project_skills
|
|
80
|
+
_workspace_root, _candidates, coordinator = project_skill_trust_coordinator
|
|
81
|
+
coordinator.remove_workspace!
|
|
82
|
+
@interactive_project_skill_paths = []
|
|
83
|
+
runtime_output("Project skill trust removed for this workspace. Use /new to rebuild the agent.")
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -8,13 +8,16 @@ module Kward
|
|
|
8
8
|
module PromptInterfaceSupport
|
|
9
9
|
private
|
|
10
10
|
|
|
11
|
-
def setup_interactive_prompt
|
|
11
|
+
def setup_interactive_prompt(defer_warnings: false)
|
|
12
12
|
return unless @stdin.tty?
|
|
13
13
|
return unless @prompt.is_a?(TTY::Prompt)
|
|
14
14
|
|
|
15
15
|
prompt_interface = load_prompt_interface
|
|
16
16
|
return unless prompt_interface
|
|
17
17
|
|
|
18
|
+
@interactive_warning_sink_active = true
|
|
19
|
+
@interactive_warning_output_ready = !defer_warnings
|
|
20
|
+
ConfigFiles.warning_sink = interactive_warning_sink
|
|
18
21
|
@prompt = prompt_interface.new(
|
|
19
22
|
slash_commands: slash_command_entries,
|
|
20
23
|
overlay_settings: ConfigFiles.overlay_settings,
|
|
@@ -41,13 +44,41 @@ module Kward
|
|
|
41
44
|
editor_line_numbers: ConfigFiles.editor_line_numbers,
|
|
42
45
|
editor_line_numbers_source: -> { ConfigFiles.editor_line_numbers },
|
|
43
46
|
diff_view: ConfigFiles.diff_view,
|
|
44
|
-
diff_view_source: -> { ConfigFiles.diff_view }
|
|
47
|
+
diff_view_source: -> { ConfigFiles.diff_view },
|
|
48
|
+
redraw_handler: method(:redraw_interactive_prompt)
|
|
45
49
|
)
|
|
46
50
|
if @prompt.method(:start).parameters.any? { |kind, name| [:key, :keyreq].include?(kind) && name == :render }
|
|
47
51
|
@prompt.start(render: false)
|
|
48
52
|
else
|
|
49
53
|
@prompt.start
|
|
50
54
|
end
|
|
55
|
+
flush_interactive_warnings if @interactive_warning_output_ready
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def enable_interactive_warnings
|
|
59
|
+
@interactive_warning_output_ready = true
|
|
60
|
+
flush_interactive_warnings
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def interactive_warning_sink
|
|
64
|
+
@interactive_warning_sink ||= lambda do |message|
|
|
65
|
+
unless @interactive_warning_sink_active
|
|
66
|
+
warn message
|
|
67
|
+
next
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if @interactive_warning_output_ready && prompt_interface?
|
|
71
|
+
runtime_output(message)
|
|
72
|
+
else
|
|
73
|
+
(@pending_interactive_warnings ||= []) << message
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def flush_interactive_warnings
|
|
79
|
+
warnings = @pending_interactive_warnings
|
|
80
|
+
@pending_interactive_warnings = []
|
|
81
|
+
Array(warnings).each { |message| runtime_output(message) }
|
|
51
82
|
end
|
|
52
83
|
|
|
53
84
|
def load_prompt_interface
|
|
@@ -68,6 +99,19 @@ module Kward
|
|
|
68
99
|
@prompt.respond_to?(:start_stream_block) && @prompt.respond_to?(:write_delta)
|
|
69
100
|
end
|
|
70
101
|
|
|
102
|
+
def emit_warning(message)
|
|
103
|
+
sink = ConfigFiles.warning_sink
|
|
104
|
+
sink ? sink.call(message) : warn(message)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def clear_interactive_warning_sink
|
|
108
|
+
ConfigFiles.warning_sink = nil
|
|
109
|
+
@interactive_warning_sink_active = false
|
|
110
|
+
@interactive_warning_output_ready = false
|
|
111
|
+
@pending_interactive_warnings = nil
|
|
112
|
+
@interactive_warning_sink = nil
|
|
113
|
+
end
|
|
114
|
+
|
|
71
115
|
def update_prompt_workspace_root(root)
|
|
72
116
|
return unless @prompt.respond_to?(:update_workspace_root)
|
|
73
117
|
|
|
@@ -122,10 +166,16 @@ module Kward
|
|
|
122
166
|
end
|
|
123
167
|
|
|
124
168
|
def startup_plugins_value
|
|
125
|
-
filenames = plugin_registry.paths.map { |path|
|
|
169
|
+
filenames = plugin_registry.paths.map { |path| startup_plugin_name(path) }
|
|
126
170
|
filenames.empty? ? "none" : filenames.join(", ")
|
|
127
171
|
end
|
|
128
172
|
|
|
173
|
+
def startup_plugin_name(path)
|
|
174
|
+
plugin_root = ConfigFiles.plugin_dir
|
|
175
|
+
prefix = "#{plugin_root}#{File::SEPARATOR}"
|
|
176
|
+
path.start_with?(prefix) ? path.delete_prefix(prefix) : File.basename(path)
|
|
177
|
+
end
|
|
178
|
+
|
|
129
179
|
def startup_status_line(refresh_update_check: false)
|
|
130
180
|
color = startup_update_notice(refresh: refresh_update_check) ? :yellow : :green
|
|
131
181
|
"#{ANSI.colorize("●", color, enabled: @color_enabled)} Kward v#{Kward::VERSION} is online."
|
|
@@ -179,7 +229,7 @@ module Kward
|
|
|
179
229
|
context = plugin_context(current_footer_conversation, "")
|
|
180
230
|
renderer.call(context).to_s
|
|
181
231
|
rescue StandardError => e
|
|
182
|
-
|
|
232
|
+
emit_warning "Warning: Kward plugin footer error: #{e.message}"
|
|
183
233
|
""
|
|
184
234
|
end
|
|
185
235
|
end
|
data/lib/kward/cli/rendering.rb
CHANGED
|
@@ -10,6 +10,24 @@ module Kward
|
|
|
10
10
|
render_transcript_messages(conversation.messages)
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
def redraw_interactive_prompt
|
|
14
|
+
tab = active_tab if respond_to?(:active_tab, true)
|
|
15
|
+
if tab && (tab.running? || tab.shell)
|
|
16
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
17
|
+
return
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
conversation = tab&.agent&.conversation || @footer_conversation
|
|
21
|
+
unless conversation && @prompt.respond_to?(:restore_transcript)
|
|
22
|
+
@prompt.redraw if @prompt.respond_to?(:redraw)
|
|
23
|
+
return
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
restore_prompt_transcript do
|
|
27
|
+
tab ? render_transcript_messages(tab.driver.messages) : render_conversation_transcript(conversation)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
13
31
|
# Renders a transcript supplied by either a Kward conversation or a
|
|
14
32
|
# plugin-owned tab driver.
|
|
15
33
|
def render_transcript_messages(messages)
|
|
@@ -199,7 +217,7 @@ module Kward
|
|
|
199
217
|
# Writes the user transcript output for the terminal CLI flow.
|
|
200
218
|
def print_user_transcript(input, display_input: nil, attachment_references: nil, image_parts: nil)
|
|
201
219
|
visible_input = display_input.nil? ? input : display_input
|
|
202
|
-
|
|
220
|
+
write_prompt_transcript("\n#{colored("You>", :blue, :bold)} #{visible_input}\n")
|
|
203
221
|
print_attachment_badges(input, references: attachment_references)
|
|
204
222
|
print_pasted_images(input, image_parts: image_parts)
|
|
205
223
|
end
|
|
@@ -209,7 +227,15 @@ module Kward
|
|
|
209
227
|
badges = references ? Array(references).map { |reference| attachment_badge_text(reference) } : composer_attachment_badges(input)
|
|
210
228
|
return if badges.empty?
|
|
211
229
|
|
|
212
|
-
|
|
230
|
+
write_prompt_transcript("#{badges.join("\n")}\n")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def write_prompt_transcript(message)
|
|
234
|
+
if @prompt.respond_to?(:write_transcript)
|
|
235
|
+
@prompt.write_transcript(message)
|
|
236
|
+
else
|
|
237
|
+
@prompt.say(message)
|
|
238
|
+
end
|
|
213
239
|
end
|
|
214
240
|
|
|
215
241
|
def composer_attachment_badges(input, attachments = [])
|