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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/pages.yml +1 -1
  3. data/CHANGELOG.md +89 -3
  4. data/Gemfile +2 -0
  5. data/Gemfile.lock +90 -2
  6. data/README.md +34 -6
  7. data/Rakefile +96 -0
  8. data/doc/agent-tools.md +52 -0
  9. data/doc/api.md +92 -0
  10. data/doc/authentication.md +58 -23
  11. data/doc/code-search.md +42 -2
  12. data/doc/configuration.md +102 -13
  13. data/doc/context-budgeting.md +136 -0
  14. data/doc/context-tools.md +83 -0
  15. data/doc/editor.md +394 -0
  16. data/doc/extensibility.md +16 -7
  17. data/doc/files.md +100 -0
  18. data/doc/getting-started.md +25 -18
  19. data/doc/git.md +122 -0
  20. data/doc/memory.md +24 -4
  21. data/doc/personas.md +34 -5
  22. data/doc/plugins.md +74 -3
  23. data/doc/releasing.md +45 -8
  24. data/doc/rpc.md +77 -15
  25. data/doc/session-management.md +254 -0
  26. data/doc/shell.md +286 -0
  27. data/doc/tabs.md +122 -0
  28. data/doc/troubleshooting.md +77 -1
  29. data/doc/usage.md +60 -15
  30. data/doc/web-search.md +12 -4
  31. data/doc/workspace-tools.md +144 -0
  32. data/examples/plugins/space_invaders.rb +377 -0
  33. data/lib/kward/agent.rb +1 -1
  34. data/lib/kward/cli/commands.rb +41 -2
  35. data/lib/kward/cli/git.rb +150 -0
  36. data/lib/kward/cli/interactive_turn.rb +73 -9
  37. data/lib/kward/cli/openrouter_commands.rb +55 -0
  38. data/lib/kward/cli/plugins.rb +54 -4
  39. data/lib/kward/cli/prompt_interface.rb +111 -6
  40. data/lib/kward/cli/rendering.rb +11 -6
  41. data/lib/kward/cli/runtime_helpers.rb +133 -3
  42. data/lib/kward/cli/sessions.rb +262 -13
  43. data/lib/kward/cli/settings.rb +216 -37
  44. data/lib/kward/cli/slash_commands.rb +439 -8
  45. data/lib/kward/cli/tabs.rb +695 -0
  46. data/lib/kward/cli.rb +171 -26
  47. data/lib/kward/compactor.rb +4 -1
  48. data/lib/kward/config_files.rb +125 -5
  49. data/lib/kward/context_budget_meter.rb +44 -0
  50. data/lib/kward/conversation.rb +59 -22
  51. data/lib/kward/editor_mode.rb +25 -0
  52. data/lib/kward/ekwsh.rb +362 -0
  53. data/lib/kward/model/client.rb +37 -50
  54. data/lib/kward/model/context_usage.rb +13 -6
  55. data/lib/kward/model/model_info.rb +92 -16
  56. data/lib/kward/model/payloads.rb +2 -0
  57. data/lib/kward/openrouter_model_cache.rb +120 -0
  58. data/lib/kward/plugin_registry.rb +108 -1
  59. data/lib/kward/project_files.rb +52 -0
  60. data/lib/kward/prompt_history.rb +82 -0
  61. data/lib/kward/prompt_interface/banner.rb +16 -51
  62. data/lib/kward/prompt_interface/composer_controller.rb +124 -83
  63. data/lib/kward/prompt_interface/composer_renderer.rb +116 -14
  64. data/lib/kward/prompt_interface/composer_state.rb +96 -27
  65. data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
  66. data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
  67. data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
  68. data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
  69. data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
  70. data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
  71. data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
  72. data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
  73. data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
  74. data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
  75. data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
  76. data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
  77. data/lib/kward/prompt_interface/editor/search.rb +76 -0
  78. data/lib/kward/prompt_interface/editor/selections.rb +120 -0
  79. data/lib/kward/prompt_interface/editor/state.rb +1249 -0
  80. data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
  81. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
  82. data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
  83. data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
  84. data/lib/kward/prompt_interface/file_overlay.rb +211 -0
  85. data/lib/kward/prompt_interface/git_prompt.rb +299 -0
  86. data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
  87. data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
  88. data/lib/kward/prompt_interface/interactive/state.rb +62 -0
  89. data/lib/kward/prompt_interface/key_handler.rb +416 -43
  90. data/lib/kward/prompt_interface/layout.rb +2 -2
  91. data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
  92. data/lib/kward/prompt_interface/project_browser.rb +524 -0
  93. data/lib/kward/prompt_interface/prompt_renderer.rb +32 -13
  94. data/lib/kward/prompt_interface/question_prompt.rb +122 -82
  95. data/lib/kward/prompt_interface/runtime_state.rb +49 -1
  96. data/lib/kward/prompt_interface/screen.rb +17 -0
  97. data/lib/kward/prompt_interface/selection_prompt.rb +511 -58
  98. data/lib/kward/prompt_interface/stream_state.rb +7 -0
  99. data/lib/kward/prompt_interface/transcript_buffer.rb +13 -16
  100. data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
  101. data/lib/kward/prompt_interface.rb +307 -35
  102. data/lib/kward/prompts/commands.rb +7 -1
  103. data/lib/kward/prompts.rb +4 -2
  104. data/lib/kward/rpc/server.rb +45 -11
  105. data/lib/kward/rpc/session_manager.rb +52 -53
  106. data/lib/kward/rpc/session_tree_rows.rb +9 -115
  107. data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
  108. data/lib/kward/session_store.rb +67 -4
  109. data/lib/kward/session_tree_nodes.rb +136 -0
  110. data/lib/kward/session_tree_renderer.rb +9 -131
  111. data/lib/kward/tab_store.rb +47 -0
  112. data/lib/kward/telemetry/logger.rb +5 -3
  113. data/lib/kward/text_boundary.rb +25 -0
  114. data/lib/kward/tool_output_compactor.rb +127 -0
  115. data/lib/kward/tools/base.rb +8 -2
  116. data/lib/kward/tools/context_budget_stats.rb +54 -0
  117. data/lib/kward/tools/context_for_task.rb +202 -0
  118. data/lib/kward/tools/read_file.rb +8 -4
  119. data/lib/kward/tools/registry.rb +92 -15
  120. data/lib/kward/tools/retrieve_tool_output.rb +71 -0
  121. data/lib/kward/tools/search/web.rb +2 -2
  122. data/lib/kward/tools/summarize_file_structure.rb +29 -0
  123. data/lib/kward/tools/tool_call.rb +12 -0
  124. data/lib/kward/version.rb +1 -1
  125. data/lib/kward/workers/git_guard.rb +68 -0
  126. data/lib/kward/workers/live_view.rb +49 -0
  127. data/lib/kward/workers/manager.rb +288 -0
  128. data/lib/kward/workers/store.rb +72 -0
  129. data/lib/kward/workers/tool_policy.rb +23 -0
  130. data/lib/kward/workers/worker.rb +82 -0
  131. data/lib/kward/workers/write_lock.rb +38 -0
  132. data/lib/kward/workers.rb +7 -0
  133. data/lib/kward/workspace.rb +154 -12
  134. data/templates/default/fulldoc/html/css/kward.css +362 -42
  135. data/templates/default/fulldoc/html/full_list.erb +107 -0
  136. data/templates/default/fulldoc/html/js/kward.js +161 -2
  137. data/templates/default/fulldoc/html/setup.rb +8 -0
  138. data/templates/default/kward_navigation.rb +102 -0
  139. data/templates/default/layout/html/layout.erb +43 -10
  140. data/templates/default/layout/html/setup.rb +39 -38
  141. metadata +65 -3
  142. data/lib/kward/resources/avatar_kward_logo.rb +0 -50
  143. 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 logo and message renderer.
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:, pixels:, screen_height:, minimum_composer_rows: 3)
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
- return [] unless visible?(width)
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
- rows = []
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(width)
36
- logo_width, logo_height = logo_dimensions(width)
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 visible?(width)
46
- !@message.empty? || image_visible?(width)
47
- end
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
- def logo_dimensions(width)
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 max_logo_height
66
- message_rows = @message.empty? ? 0 : 1
67
- blank_after_banner = 1
36
+ def max_banner_rows
68
37
  transcript_row = 1
69
- reserved_rows = message_rows + blank_after_banner + @minimum_composer_rows + transcript_row
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