kward 0.70.0 → 0.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/pages.yml +1 -1
- data/CHANGELOG.md +89 -3
- data/Gemfile +2 -0
- data/Gemfile.lock +90 -2
- data/README.md +34 -6
- data/Rakefile +96 -0
- data/doc/agent-tools.md +52 -0
- data/doc/api.md +92 -0
- data/doc/authentication.md +58 -23
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +102 -13
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +83 -0
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +74 -3
- data/doc/releasing.md +45 -8
- data/doc/rpc.md +77 -15
- data/doc/session-management.md +254 -0
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +60 -15
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +144 -0
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +41 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/openrouter_commands.rb +55 -0
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +111 -6
- data/lib/kward/cli/rendering.rb +11 -6
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +262 -13
- data/lib/kward/cli/settings.rb +216 -37
- data/lib/kward/cli/slash_commands.rb +439 -8
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +171 -26
- data/lib/kward/compactor.rb +4 -1
- data/lib/kward/config_files.rb +125 -5
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +59 -22
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/model/client.rb +37 -50
- data/lib/kward/model/context_usage.rb +13 -6
- data/lib/kward/model/model_info.rb +92 -16
- data/lib/kward/model/payloads.rb +2 -0
- data/lib/kward/openrouter_model_cache.rb +120 -0
- data/lib/kward/plugin_registry.rb +108 -1
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/banner.rb +16 -51
- data/lib/kward/prompt_interface/composer_controller.rb +124 -83
- data/lib/kward/prompt_interface/composer_renderer.rb +116 -14
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +416 -43
- data/lib/kward/prompt_interface/layout.rb +2 -2
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/prompt_renderer.rb +32 -13
- data/lib/kward/prompt_interface/question_prompt.rb +122 -82
- data/lib/kward/prompt_interface/runtime_state.rb +49 -1
- data/lib/kward/prompt_interface/screen.rb +17 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +511 -58
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +13 -16
- data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
- data/lib/kward/prompt_interface.rb +307 -35
- data/lib/kward/prompts/commands.rb +7 -1
- data/lib/kward/prompts.rb +4 -2
- data/lib/kward/rpc/server.rb +45 -11
- data/lib/kward/rpc/session_manager.rb +52 -53
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +67 -4
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/telemetry/logger.rb +5 -3
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tool_output_compactor.rb +127 -0
- data/lib/kward/tools/base.rb +8 -2
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +92 -15
- data/lib/kward/tools/retrieve_tool_output.rb +71 -0
- data/lib/kward/tools/search/web.rb +2 -2
- data/lib/kward/tools/summarize_file_structure.rb +29 -0
- data/lib/kward/tools/tool_call.rb +12 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +154 -12
- data/templates/default/fulldoc/html/css/kward.css +362 -42
- data/templates/default/fulldoc/html/full_list.erb +107 -0
- data/templates/default/fulldoc/html/js/kward.js +161 -2
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/kward_navigation.rb +102 -0
- data/templates/default/layout/html/layout.erb +43 -10
- data/templates/default/layout/html/setup.rb +39 -38
- metadata +65 -3
- data/lib/kward/resources/avatar_kward_logo.rb +0 -50
- data/lib/kward/resources/pixel_logo.rb +0 -232
|
@@ -23,6 +23,15 @@ module Kward
|
|
|
23
23
|
end
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
+
# Registered interactive command that takes over the composer region with a
|
|
27
|
+
# Kward-driven render and input loop. Like a slash command but with canvas
|
|
28
|
+
# rendering capabilities for games, dashboards, viewers, and similar uses.
|
|
29
|
+
InteractiveCommand = Struct.new(:name, :description, :argument_hint, :rows, :fps, :path, :handler, keyword_init: true) do
|
|
30
|
+
def entry
|
|
31
|
+
{ name: name, description: description, argument_hint: argument_hint }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
26
35
|
# Read-only event passed to plugin transcript observers.
|
|
27
36
|
TranscriptEvent = Struct.new(:type, :payload, keyword_init: true) do
|
|
28
37
|
def to_h
|
|
@@ -95,7 +104,19 @@ module Kward
|
|
|
95
104
|
end
|
|
96
105
|
end
|
|
97
106
|
|
|
98
|
-
# DSL object yielded by `Kward.plugin` blocks.
|
|
107
|
+
# Public DSL object yielded by `Kward.plugin` blocks.
|
|
108
|
+
#
|
|
109
|
+
# Plugin files normally interact with this object only through a block:
|
|
110
|
+
#
|
|
111
|
+
# @example Register a plugin command
|
|
112
|
+
# Kward.plugin do |plugin|
|
|
113
|
+
# plugin.command "hello", description: "Say hello" do |args, ctx|
|
|
114
|
+
# name = args.strip.empty? ? "there" : args.strip
|
|
115
|
+
# ctx.say "Hello, #{name}."
|
|
116
|
+
# end
|
|
117
|
+
# end
|
|
118
|
+
#
|
|
119
|
+
# @api public
|
|
99
120
|
class DSL
|
|
100
121
|
# Creates an object for trusted plugin loading and dispatch.
|
|
101
122
|
def initialize(registry, path)
|
|
@@ -105,36 +126,74 @@ module Kward
|
|
|
105
126
|
|
|
106
127
|
# Registers a slash command.
|
|
107
128
|
#
|
|
129
|
+
# The command is available in the interactive CLI and through the RPC
|
|
130
|
+
# command bridge. Command names do not include the leading `/`.
|
|
131
|
+
#
|
|
108
132
|
# @param name [String, #to_s] command name without the leading slash
|
|
109
133
|
# @param description [String] short text shown in command listings
|
|
110
134
|
# @param argument_hint [String] optional usage hint for arguments
|
|
111
135
|
# @yieldparam args [String] text after the command name
|
|
112
136
|
# @yieldparam ctx [Context] plugin execution context
|
|
137
|
+
# @return [void]
|
|
138
|
+
# @api public
|
|
113
139
|
def command(name, description: "", argument_hint: "", &block)
|
|
114
140
|
@registry.register_command(name, description: description, argument_hint: argument_hint, path: @path, &block)
|
|
115
141
|
end
|
|
116
142
|
|
|
117
143
|
# Registers or replaces the custom footer renderer.
|
|
118
144
|
#
|
|
145
|
+
# Only one footer renderer is active. If multiple plugins register one,
|
|
146
|
+
# the later renderer replaces the earlier renderer.
|
|
147
|
+
#
|
|
119
148
|
# @yieldparam ctx [Context] plugin execution context
|
|
149
|
+
# @return [void]
|
|
150
|
+
# @api public
|
|
120
151
|
def footer(&block)
|
|
121
152
|
@registry.register_footer(path: @path, &block)
|
|
122
153
|
end
|
|
123
154
|
|
|
124
155
|
# Registers a live transcript event observer.
|
|
125
156
|
#
|
|
157
|
+
# Observer errors are caught and reported as warnings so a plugin cannot
|
|
158
|
+
# crash the active turn by raising from an event handler.
|
|
159
|
+
#
|
|
126
160
|
# @yieldparam event [TranscriptEvent] normalized transcript event
|
|
127
161
|
# @yieldparam ctx [Context] plugin execution context
|
|
162
|
+
# @return [void]
|
|
163
|
+
# @api public
|
|
128
164
|
def on_transcript_event(&block)
|
|
129
165
|
@registry.register_transcript_event(path: @path, &block)
|
|
130
166
|
end
|
|
131
167
|
|
|
132
168
|
# Registers prompt context text injected into future system prompts.
|
|
133
169
|
#
|
|
170
|
+
# Keep this text short and never include secrets. The returned string can
|
|
171
|
+
# be sent to the active model as part of Kward's system instructions.
|
|
172
|
+
#
|
|
134
173
|
# @yieldparam ctx [Context] plugin execution context
|
|
174
|
+
# @return [void]
|
|
175
|
+
# @api public
|
|
135
176
|
def prompt_context(&block)
|
|
136
177
|
@registry.register_prompt_context(path: @path, &block)
|
|
137
178
|
end
|
|
179
|
+
|
|
180
|
+
# Registers an interactive command that takes over the composer region with
|
|
181
|
+
# a Kward-driven render and input loop. The handler receives an
|
|
182
|
+
# interactive controller object with a canvas API for drawing colored
|
|
183
|
+
# cells and reading keys. Useful for games, dashboards, and viewers.
|
|
184
|
+
#
|
|
185
|
+
# @param name [String, #to_s] command name without the leading slash
|
|
186
|
+
# @param rows [Integer] fixed canvas height in terminal rows
|
|
187
|
+
# @param fps [Numeric] frame rate for tick callbacks (1-120, default 30)
|
|
188
|
+
# @param description [String] short text shown in command listings
|
|
189
|
+
# @param argument_hint [String] optional usage hint for arguments
|
|
190
|
+
# @yieldparam ui [Object] interactive controller with canvas and key API
|
|
191
|
+
# @yieldparam ctx [Context] plugin execution context
|
|
192
|
+
# @return [void]
|
|
193
|
+
# @api public
|
|
194
|
+
def interactive_command(name, rows:, fps: 30, description: "", argument_hint: "", &block)
|
|
195
|
+
@registry.register_interactive_command(name, rows: rows, fps: fps, description: description, argument_hint: argument_hint, path: @path, &block)
|
|
196
|
+
end
|
|
138
197
|
end
|
|
139
198
|
|
|
140
199
|
# Mutable singleton guard used while loading trusted plugin files.
|
|
@@ -175,15 +234,20 @@ module Kward
|
|
|
175
234
|
def initialize(reserved_commands: [])
|
|
176
235
|
@reserved_commands = reserved_commands.map(&:to_s)
|
|
177
236
|
@commands = {}
|
|
237
|
+
@interactive_commands = {}
|
|
178
238
|
@footer = nil
|
|
179
239
|
@footer_path = nil
|
|
180
240
|
@transcript_event_handlers = []
|
|
181
241
|
@prompt_context_renderers = []
|
|
242
|
+
@paths = []
|
|
182
243
|
end
|
|
183
244
|
|
|
184
245
|
# @return [String, nil] plugin file currently responsible for footer output
|
|
185
246
|
attr_reader :footer_path
|
|
186
247
|
|
|
248
|
+
# @return [Array<String>] plugin files successfully loaded by this registry
|
|
249
|
+
attr_reader :paths
|
|
250
|
+
|
|
187
251
|
def commands
|
|
188
252
|
@commands.values
|
|
189
253
|
end
|
|
@@ -192,6 +256,14 @@ module Kward
|
|
|
192
256
|
@commands[name.to_s]
|
|
193
257
|
end
|
|
194
258
|
|
|
259
|
+
def interactive_commands
|
|
260
|
+
@interactive_commands.values
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def interactive_command_for(name)
|
|
264
|
+
@interactive_commands[name.to_s]
|
|
265
|
+
end
|
|
266
|
+
|
|
195
267
|
def footer_renderer
|
|
196
268
|
@footer
|
|
197
269
|
end
|
|
@@ -233,6 +305,7 @@ module Kward
|
|
|
233
305
|
self.class.loading_registry = self
|
|
234
306
|
self.class.loading_path = path
|
|
235
307
|
Kernel.load(path)
|
|
308
|
+
@paths << path
|
|
236
309
|
rescue StandardError => e
|
|
237
310
|
warn "Warning: skipping Kward plugin #{path}: #{e.message}"
|
|
238
311
|
ensure
|
|
@@ -269,6 +342,31 @@ module Kward
|
|
|
269
342
|
)
|
|
270
343
|
end
|
|
271
344
|
|
|
345
|
+
def register_interactive_command(name, rows:, fps: 30, description: "", argument_hint: "", path: nil, &handler)
|
|
346
|
+
name = name.to_s
|
|
347
|
+
raise "Interactive command name is invalid: #{name}" unless name.match?(COMMAND_NAME_PATTERN)
|
|
348
|
+
raise "Interactive command /#{name} requires a handler" unless handler
|
|
349
|
+
|
|
350
|
+
if @reserved_commands.include?(name) || @commands.key?(name)
|
|
351
|
+
warn "Warning: skipping Kward interactive command /#{name}: reserved command"
|
|
352
|
+
return nil
|
|
353
|
+
end
|
|
354
|
+
if @interactive_commands.key?(name)
|
|
355
|
+
warn "Warning: skipping duplicate Kward interactive command /#{name}: #{path}"
|
|
356
|
+
return nil
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
@interactive_commands[name] = InteractiveCommand.new(
|
|
360
|
+
name: name,
|
|
361
|
+
description: description.to_s,
|
|
362
|
+
argument_hint: argument_hint.to_s,
|
|
363
|
+
rows: [[rows.to_i, 1].max, 1].max,
|
|
364
|
+
fps: [[fps.to_f, 1].max, 120].min,
|
|
365
|
+
path: path,
|
|
366
|
+
handler: handler
|
|
367
|
+
)
|
|
368
|
+
end
|
|
369
|
+
|
|
272
370
|
def register_footer(path: nil, &renderer)
|
|
273
371
|
raise "Plugin footer requires a renderer" unless renderer
|
|
274
372
|
|
|
@@ -329,6 +427,15 @@ module Kward
|
|
|
329
427
|
end
|
|
330
428
|
end
|
|
331
429
|
|
|
430
|
+
# Registers a trusted local plugin.
|
|
431
|
+
#
|
|
432
|
+
# This method is intended for Ruby files loaded from the user plugin
|
|
433
|
+
# directory. It raises if called outside plugin loading so workspace code
|
|
434
|
+
# cannot silently mutate Kward's runtime by merely being required.
|
|
435
|
+
#
|
|
436
|
+
# @yieldparam plugin [PluginRegistry::DSL] plugin registration DSL
|
|
437
|
+
# @return [Object, nil] the plugin block result
|
|
438
|
+
# @api public
|
|
332
439
|
def self.plugin(&block)
|
|
333
440
|
registry = PluginRegistry.loading_registry
|
|
334
441
|
raise "Kward.plugin can only be called while loading a plugin" unless registry
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
require "find"
|
|
2
|
+
require "open3"
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
# Namespace for the Kward CLI agent runtime.
|
|
6
|
+
module Kward
|
|
7
|
+
# Discovers project files for prompt UI features.
|
|
8
|
+
module ProjectFiles
|
|
9
|
+
module_function
|
|
10
|
+
|
|
11
|
+
def list(root: Dir.pwd)
|
|
12
|
+
paths = git_paths(root)
|
|
13
|
+
paths = scanned_paths(root) if paths.empty?
|
|
14
|
+
paths.reject { |path| path.empty? || path.end_with?("/") }.uniq.sort
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def git_paths(root)
|
|
18
|
+
output, status = Open3.capture2("git", "ls-files", "--cached", "--others", "--exclude-standard", chdir: root)
|
|
19
|
+
return [] unless status.success?
|
|
20
|
+
|
|
21
|
+
output.lines.map(&:chomp).reject(&:empty?)
|
|
22
|
+
rescue StandardError
|
|
23
|
+
[]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def scanned_paths(root)
|
|
27
|
+
root_path = Pathname.new(root)
|
|
28
|
+
paths = []
|
|
29
|
+
Find.find(root_path.to_s) do |path|
|
|
30
|
+
relative = Pathname.new(path).relative_path_from(root_path).to_s
|
|
31
|
+
if File.directory?(path)
|
|
32
|
+
Find.prune if ignored_directory?(relative)
|
|
33
|
+
next
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
paths << relative unless ignored_file?(relative)
|
|
37
|
+
end
|
|
38
|
+
paths
|
|
39
|
+
rescue StandardError
|
|
40
|
+
[]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def ignored_directory?(relative)
|
|
44
|
+
ignored_directories = %w[.git .yardoc _yardoc node_modules rdoc tmp vendor/bundle]
|
|
45
|
+
ignored_directories.include?(relative) || relative.start_with?(".git/")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def ignored_file?(relative)
|
|
49
|
+
relative.start_with?(".git/")
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "json"
|
|
3
|
+
require "time"
|
|
4
|
+
require_relative "config_files"
|
|
5
|
+
|
|
6
|
+
# Namespace for the Kward CLI agent runtime.
|
|
7
|
+
module Kward
|
|
8
|
+
# Workspace-scoped JSONL persistence for terminal prompt history.
|
|
9
|
+
class PromptHistory
|
|
10
|
+
DEFAULT_LIMIT = 1_000
|
|
11
|
+
|
|
12
|
+
Entry = Struct.new(:value, :timestamp, keyword_init: true)
|
|
13
|
+
|
|
14
|
+
def initialize(config_dir: ConfigFiles.config_dir, cwd: Dir.pwd, limit: DEFAULT_LIMIT)
|
|
15
|
+
@config_dir = config_dir
|
|
16
|
+
@cwd = ConfigFiles.canonical_workspace_root(cwd)
|
|
17
|
+
@limit = limit.to_i.positive? ? limit.to_i : DEFAULT_LIMIT
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
attr_reader :cwd, :limit
|
|
21
|
+
|
|
22
|
+
def values
|
|
23
|
+
entries.map(&:value)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def append(value)
|
|
27
|
+
text = value.to_s
|
|
28
|
+
return false if text.strip.empty?
|
|
29
|
+
|
|
30
|
+
existing = entries
|
|
31
|
+
return false if existing.last&.value == text
|
|
32
|
+
|
|
33
|
+
write_entries((existing + [Entry.new(value: text, timestamp: Time.now.utc.iso8601(3))]).last(limit))
|
|
34
|
+
true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def path
|
|
38
|
+
ConfigFiles.prompt_history_path(@cwd, config_dir: @config_dir)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def entries
|
|
44
|
+
return [] unless File.file?(path)
|
|
45
|
+
|
|
46
|
+
File.readlines(path, chomp: true).filter_map do |line|
|
|
47
|
+
record = JSON.parse(line)
|
|
48
|
+
value = record["value"].to_s
|
|
49
|
+
next if value.strip.empty?
|
|
50
|
+
|
|
51
|
+
Entry.new(value: value, timestamp: record["timestamp"].to_s)
|
|
52
|
+
rescue JSON::ParserError
|
|
53
|
+
nil
|
|
54
|
+
end.last(limit)
|
|
55
|
+
rescue Errno::ENOENT
|
|
56
|
+
[]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def write_entries(entries)
|
|
60
|
+
FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
|
|
61
|
+
File.open(path, File::WRONLY | File::CREAT | File::TRUNC, 0o600) do |file|
|
|
62
|
+
file.write(JSON.generate(history_header))
|
|
63
|
+
file.write("\n")
|
|
64
|
+
entries.each do |entry|
|
|
65
|
+
file.write(JSON.generate({ type: "prompt_history", version: 1, timestamp: entry.timestamp || Time.now.utc.iso8601(3), value: entry.value }))
|
|
66
|
+
file.write("\n")
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
File.chmod(0o600, path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def history_header
|
|
73
|
+
{
|
|
74
|
+
type: "prompt_history_header",
|
|
75
|
+
version: 1,
|
|
76
|
+
workspace: @cwd,
|
|
77
|
+
workspaceHash: File.basename(path, ".jsonl"),
|
|
78
|
+
limit: limit
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -1,80 +1,45 @@
|
|
|
1
|
-
require_relative "../ansi"
|
|
2
|
-
require_relative "../resources/avatar_kward_logo"
|
|
3
|
-
require_relative "../resources/pixel_logo"
|
|
4
|
-
|
|
5
1
|
# Namespace for the Kward CLI agent runtime.
|
|
6
2
|
module Kward
|
|
7
|
-
# Startup banner
|
|
3
|
+
# Startup banner message renderer.
|
|
8
4
|
class PromptInterface
|
|
9
5
|
# Startup banner rendering data and helpers for the prompt interface.
|
|
10
6
|
class Banner
|
|
11
|
-
LOGO_WIDTH = 32
|
|
12
|
-
LOGO_PIXEL_HEIGHT = 32
|
|
13
|
-
MIN_LOGO_HEIGHT = 4
|
|
14
|
-
LOGO_PIXELS = Kward::Resources::AvatarKwardLogo::PIXELS
|
|
15
7
|
MESSAGE = "State your business.".freeze
|
|
16
8
|
|
|
17
|
-
def initialize(message:,
|
|
9
|
+
def initialize(message:, screen_height:, minimum_composer_rows: 3)
|
|
18
10
|
@message = message.to_s
|
|
19
|
-
@pixels = pixels
|
|
20
11
|
@screen_height = screen_height
|
|
21
12
|
@minimum_composer_rows = minimum_composer_rows
|
|
22
|
-
@logo_cache = {}
|
|
23
13
|
end
|
|
24
14
|
|
|
25
|
-
def rows(width)
|
|
26
|
-
|
|
15
|
+
def rows(width, message: nil)
|
|
16
|
+
content = message.nil? ? @message : message.to_s
|
|
17
|
+
return [] if content.empty? || max_banner_rows <= 0
|
|
27
18
|
|
|
28
|
-
|
|
29
|
-
rows.concat(centered_image_rows(width)) if image_visible?(width)
|
|
30
|
-
rows << align_plain_row(@message, width) unless @message.empty?
|
|
31
|
-
rows << ""
|
|
32
|
-
rows
|
|
19
|
+
visible_lines(content) + [""]
|
|
33
20
|
end
|
|
34
21
|
|
|
35
|
-
def logo_rows(
|
|
36
|
-
|
|
37
|
-
return [] unless @pixels && max_logo_height >= MIN_LOGO_HEIGHT
|
|
38
|
-
|
|
39
|
-
key = [logo_width, logo_height]
|
|
40
|
-
@logo_cache[key] ||= Kward::PixelLogo.half_block_rows_from_pixels(@pixels, width: logo_width, pixel_height: logo_height)
|
|
22
|
+
def logo_rows(_width)
|
|
23
|
+
[]
|
|
41
24
|
end
|
|
42
25
|
|
|
43
26
|
private
|
|
44
27
|
|
|
45
|
-
def
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def image_visible?(width)
|
|
50
|
-
!logo_rows(width).empty?
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def centered_image_rows(width)
|
|
54
|
-
logo_width, = logo_dimensions(width)
|
|
55
|
-
padding = [[(width - logo_width) / 2, 0].max, width - 1].min
|
|
56
|
-
logo_rows(width).map { |row| (" " * padding) + row }
|
|
57
|
-
end
|
|
28
|
+
def visible_lines(content)
|
|
29
|
+
lines = content.lines(chomp: true)
|
|
30
|
+
return lines if lines.length <= max_banner_rows
|
|
31
|
+
return [lines.last] if max_banner_rows == 1
|
|
58
32
|
|
|
59
|
-
|
|
60
|
-
logo_width = [LOGO_WIDTH, [width - 2, 1].max].min
|
|
61
|
-
logo_height = [LOGO_PIXEL_HEIGHT, max_logo_height * 2].min
|
|
62
|
-
[logo_width, logo_height]
|
|
33
|
+
lines.first(max_banner_rows - 1) + [lines.last]
|
|
63
34
|
end
|
|
64
35
|
|
|
65
|
-
def
|
|
66
|
-
message_rows = @message.empty? ? 0 : 1
|
|
67
|
-
blank_after_banner = 1
|
|
36
|
+
def max_banner_rows
|
|
68
37
|
transcript_row = 1
|
|
69
|
-
|
|
38
|
+
blank_after_banner = 1
|
|
39
|
+
reserved_rows = blank_after_banner + @minimum_composer_rows + transcript_row
|
|
70
40
|
[@screen_height.call - reserved_rows, 0].max
|
|
71
41
|
end
|
|
72
42
|
|
|
73
|
-
def align_plain_row(text, width)
|
|
74
|
-
plain_length = ANSI.strip(text).length
|
|
75
|
-
padding = [width - plain_length, 0].max / 2
|
|
76
|
-
(" " * padding) + text.to_s
|
|
77
|
-
end
|
|
78
43
|
end
|
|
79
44
|
end
|
|
80
45
|
end
|