kward 0.84.0 → 0.85.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 (155) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +2 -2
  5. data/doc/agent-tools.md +13 -1
  6. data/doc/api.md +17 -2
  7. data/doc/composer.md +1 -1
  8. data/doc/configuration.md +27 -4
  9. data/doc/extensibility.md +2 -1
  10. data/doc/git.md +3 -1
  11. data/doc/pan.md +6 -0
  12. data/doc/permissions.md +4 -4
  13. data/doc/plugins.md +464 -15
  14. data/doc/rpc.md +154 -16
  15. data/doc/sandboxing.md +11 -5
  16. data/doc/security.md +7 -1
  17. data/doc/session-management.md +5 -4
  18. data/doc/tabs.md +6 -2
  19. data/doc/transports.md +15 -0
  20. data/doc/usage.md +4 -1
  21. data/doc/workspace-tools.md +9 -0
  22. data/examples/plugins/space_invaders.rb +1 -1
  23. data/examples/plugins/stardate_footer.rb +2 -2
  24. data/examples/plugins/telegram/plugin.rb +1 -1
  25. data/lib/kward/agent.rb +24 -11
  26. data/lib/kward/cli/compaction.rb +9 -3
  27. data/lib/kward/cli/plugins.rb +81 -12
  28. data/lib/kward/cli/prompt_interface.rb +25 -5
  29. data/lib/kward/cli/rendering.rb +3 -0
  30. data/lib/kward/cli/runtime_helpers.rb +121 -27
  31. data/lib/kward/cli/sessions.rb +9 -5
  32. data/lib/kward/cli/settings/menus.rb +745 -0
  33. data/lib/kward/cli/settings/model.rb +327 -0
  34. data/lib/kward/cli/settings.rb +6 -1055
  35. data/lib/kward/cli/slash_commands.rb +45 -4
  36. data/lib/kward/cli/tabs.rb +167 -27
  37. data/lib/kward/{cli_transcript_formatter.rb → cli/transcript_formatter.rb} +3 -3
  38. data/lib/kward/cli/worktrees.rb +65 -2
  39. data/lib/kward/cli.rb +26 -24
  40. data/lib/kward/compactor.rb +18 -7
  41. data/lib/kward/config/core.rb +389 -0
  42. data/lib/kward/config/extensions.rb +96 -0
  43. data/lib/kward/config/prompts.rb +313 -0
  44. data/lib/kward/config/settings.rb +250 -0
  45. data/lib/kward/config_files.rb +14 -1008
  46. data/lib/kward/conversation.rb +31 -2
  47. data/lib/kward/image_attachments.rb +1 -1
  48. data/lib/kward/model/client.rb +2 -2
  49. data/lib/kward/model/copilot_models.rb +2 -2
  50. data/lib/kward/model/model_info.rb +20 -3
  51. data/lib/kward/{openrouter_model_cache.rb → model/openrouter_model_cache.rb} +3 -3
  52. data/lib/kward/model/payloads.rb +12 -3
  53. data/lib/kward/model/typesafe_client.rb +78 -0
  54. data/lib/kward/pan/server.rb +10 -7
  55. data/lib/kward/permissions/policy.rb +6 -2
  56. data/lib/kward/plugin_registry.rb +2 -659
  57. data/lib/kward/plugins/actions.rb +453 -0
  58. data/lib/kward/plugins/chat_contract.rb +121 -0
  59. data/lib/kward/{plugin_chat_runtime.rb → plugins/chat_runtime.rb} +62 -18
  60. data/lib/kward/plugins/host.rb +232 -0
  61. data/lib/kward/plugins/registry.rb +1190 -0
  62. data/lib/kward/plugins/resources.rb +206 -0
  63. data/lib/kward/plugins/turn_request.rb +36 -0
  64. data/lib/kward/plugins/ui.rb +219 -0
  65. data/lib/kward/prompt_interface/composer_renderer.rb +1 -1
  66. data/lib/kward/prompt_interface/composer_state.rb +1 -1
  67. data/lib/kward/prompt_interface/editor/controller.rb +1 -1
  68. data/lib/kward/prompt_interface/editor/runner.rb +1 -1
  69. data/lib/kward/prompt_interface/editor/state.rb +1 -1
  70. data/lib/kward/prompt_interface/layout.rb +1 -1
  71. data/lib/kward/prompt_interface/overlay_renderer.rb +1 -1
  72. data/lib/kward/prompt_interface/plugin_ui_requests.rb +82 -0
  73. data/lib/kward/prompt_interface/runtime_state.rb +6 -1
  74. data/lib/kward/prompt_interface/screen.rb +9 -2
  75. data/lib/kward/prompt_interface.rb +60 -11
  76. data/lib/kward/prompts/commands.rb +2 -1
  77. data/lib/kward/{adaptive_pty_output_sink.rb → pty/adaptive_output_sink.rb} +1 -1
  78. data/lib/kward/{interactive_pty_runner.rb → pty/interactive_runner.rb} +2 -2
  79. data/lib/kward/{local_command_runner.rb → pty/local_command_runner.rb} +1 -1
  80. data/lib/kward/{local_pty_command_runner.rb → pty/local_pty_runner.rb} +3 -9
  81. data/lib/kward/{pty_output_sink.rb → pty/output_sink.rb} +9 -4
  82. data/lib/kward/{pty_transcript_normalizer.rb → pty/transcript_normalizer.rb} +1 -1
  83. data/lib/kward/rpc/plugin_chat_manager.rb +30 -10
  84. data/lib/kward/rpc/prompt_bridge.rb +25 -0
  85. data/lib/kward/rpc/server.rb +91 -12
  86. data/lib/kward/rpc/session_manager.rb +147 -44
  87. data/lib/kward/rpc/session_tree_rows.rb +2 -2
  88. data/lib/kward/rpc/tool_metadata.rb +1 -1
  89. data/lib/kward/sandbox/command_runner.rb +1 -1
  90. data/lib/kward/{session_catalog.rb → sessions/catalog.rb} +1 -1
  91. data/lib/kward/{session_store.rb → sessions/store.rb} +9 -9
  92. data/lib/kward/{session_tree_nodes.rb → sessions/tree_nodes.rb} +3 -3
  93. data/lib/kward/{session_tree_renderer.rb → sessions/tree_renderer.rb} +4 -4
  94. data/lib/kward/{session_tree_tool_display.rb → sessions/tree_tool_display.rb} +1 -1
  95. data/lib/kward/{kwsh.rb → shell/kwsh.rb} +3 -3
  96. data/lib/kward/{persistent_shell_session.rb → shell/persistent_session.rb} +3 -3
  97. data/lib/kward/{shell_prompt_session.rb → shell/prompt_session.rb} +1 -1
  98. data/lib/kward/skills/trust_store.rb +1 -1
  99. data/lib/kward/tabs/driver.rb +194 -0
  100. data/lib/kward/{tab_store.rb → tabs/store.rb} +2 -2
  101. data/lib/kward/{clipboard.rb → terminal/clipboard.rb} +1 -1
  102. data/lib/kward/{terminal_image_support.rb → terminal/image_support.rb} +1 -1
  103. data/lib/kward/tools/base.rb +18 -0
  104. data/lib/kward/tools/context_for_task.rb +15 -6
  105. data/lib/kward/tools/edit_file.rb +9 -6
  106. data/lib/kward/tools/git_commit.rb +13 -7
  107. data/lib/kward/tools/list_directory.rb +4 -4
  108. data/lib/kward/tools/plugin_tool.rb +41 -0
  109. data/lib/kward/tools/prepare_shell_command.rb +1 -1
  110. data/lib/kward/tools/read_file.rb +7 -6
  111. data/lib/kward/tools/registry.rb +96 -13
  112. data/lib/kward/tools/run_shell_command.rb +10 -8
  113. data/lib/kward/tools/search/code.rb +1 -1
  114. data/lib/kward/tools/summarize_file_structure.rb +5 -5
  115. data/lib/kward/tools/tool_call.rb +2 -1
  116. data/lib/kward/tools/typesafe_evaluate.rb +81 -0
  117. data/lib/kward/tools/workspace_targets.rb +58 -0
  118. data/lib/kward/tools/write_file.rb +9 -6
  119. data/lib/kward/{export_path.rb → transcripts/export_path.rb} +1 -1
  120. data/lib/kward/{markdown_transcript.rb → transcripts/markdown_transcript.rb} +2 -2
  121. data/lib/kward/transport/contracts.rb +200 -0
  122. data/lib/kward/transport/gateway.rb +79 -35
  123. data/lib/kward/transport/plugin_chat_gateway.rb +3 -2
  124. data/lib/kward/transport.rb +1 -200
  125. data/lib/kward/version.rb +1 -1
  126. data/lib/kward/{workspace_factory.rb → workspace/factory.rb} +2 -2
  127. data/lib/kward/{git_worktree_manager.rb → workspace/git_worktree_manager.rb} +28 -0
  128. data/lib/kward/{workspace.rb → workspace/workspace.rb} +3 -3
  129. data/templates/default/fulldoc/html/css/kward.css +125 -0
  130. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  131. data/templates/default/fulldoc/html/setup.rb +1 -1
  132. data/templates/default/layout/html/layout.erb +16 -4
  133. metadata +67 -48
  134. data/lib/kward/tab_driver.rb +0 -90
  135. data/templates/default/fulldoc/html/images/kward_workflow.svg +0 -52
  136. /data/lib/kward/{editor_prompt.rb → cli/editor_prompt.rb} +0 -0
  137. /data/lib/kward/{editor_prompt_session.rb → cli/editor_prompt_session.rb} +0 -0
  138. /data/lib/kward/{diff_view_mode.rb → prompt_interface/editor/diff_view_mode.rb} +0 -0
  139. /data/lib/kward/{editor_mode.rb → prompt_interface/editor/editor_mode.rb} +0 -0
  140. /data/lib/kward/{markdown_code_block.rb → prompt_interface/editor/markdown_code_block.rb} +0 -0
  141. /data/lib/kward/{scratchpad_languages.rb → prompt_interface/editor/scratchpad_languages.rb} +0 -0
  142. /data/lib/kward/{scratchpad_runner.rb → prompt_interface/editor/scratchpad_runner.rb} +0 -0
  143. /data/lib/kward/{detached_run.rb → pty/detached_run.rb} +0 -0
  144. /data/lib/kward/{session_diff.rb → sessions/diff.rb} +0 -0
  145. /data/lib/kward/{session_naming.rb → sessions/naming.rb} +0 -0
  146. /data/lib/kward/{session_trash.rb → sessions/trash.rb} +0 -0
  147. /data/lib/kward/{kwshrc.rb → shell/kwshrc.rb} +0 -0
  148. /data/lib/kward/{shell_prompt.rb → shell/prompt.rb} +0 -0
  149. /data/lib/kward/{ansi.rb → terminal/ansi.rb} +0 -0
  150. /data/lib/kward/{terminal_keys.rb → terminal/keys.rb} +0 -0
  151. /data/lib/kward/{terminal_sequences.rb → terminal/sequences.rb} +0 -0
  152. /data/lib/kward/{terminal_text.rb → terminal/text.rb} +0 -0
  153. /data/lib/kward/{transcript_export.rb → transcripts/transcript_export.rb} +0 -0
  154. /data/lib/kward/{project_files.rb → workspace/files.rb} +0 -0
  155. /data/lib/kward/{path_guard.rb → workspace/path_guard.rb} +0 -0
@@ -0,0 +1,327 @@
1
+ # CLI settings model behavior.
2
+ module Kward
3
+ class CLI
4
+ module Settings
5
+ module Model
6
+ private
7
+
8
+ def configure_model(conversation = nil, models: nil)
9
+ unless model_overlay_available?
10
+ runtime_output("Model overlay is unavailable in this prompt.")
11
+ return
12
+ end
13
+
14
+ models ||= normalized_available_models(conversation)
15
+ provider_filter = nil
16
+ show_all = false
17
+ loop do
18
+ visible_models = provider_filter ? models.select { |model| model[:provider] == provider_filter } : models
19
+ visible_models = visible_models.select { |model| default_selectable_model?(model) } unless show_all
20
+ actions = ["Refresh model list", "Enter model ID manually", "Show all models", "Change provider"]
21
+ selected = @prompt.select("Default model", model_choices(visible_models, conversation) + actions, title: "Models", custom: true)
22
+ return unless selected
23
+
24
+ case selected.to_s
25
+ when "Refresh model list"
26
+ refreshed = @client.refresh_available_models(provider: provider_filter) if @client.respond_to?(:refresh_available_models)
27
+ models = normalized_available_models(conversation, models: refreshed)
28
+ when "Enter model ID manually"
29
+ provider = provider_filter || conversation&.provider || current_model_provider
30
+ model = @prompt.select("Model ID for #{provider}", [], title: "Models", custom: true)
31
+ return unless model
32
+ persist_model_selection(provider, model, conversation)
33
+ return
34
+ when "Show all models"
35
+ provider_filter = nil
36
+ show_all = true
37
+ when "Change provider"
38
+ selected_provider_name = selected_provider(@prompt.select("Provider", provider_choices, title: "Models"))
39
+ if selected_provider_name
40
+ provider_filter = selected_provider_name
41
+ show_all = false
42
+ end
43
+ else
44
+ provider, model = selected_model(selected, models)
45
+ persist_model_selection(provider, model, conversation)
46
+ return
47
+ end
48
+ end
49
+ rescue StandardError => e
50
+ runtime_output("Model error: #{e.message}")
51
+ end
52
+
53
+ def configure_reasoning(conversation = nil)
54
+ unless model_overlay_available?
55
+ runtime_output("Reasoning overlay is unavailable in this prompt.")
56
+ return
57
+ end
58
+
59
+ provider = conversation&.provider || current_model_provider
60
+ model = conversation&.model || current_model_id
61
+ choices = ModelInfo.reasoning_effort_choices(provider, model)
62
+ if choices.empty?
63
+ runtime_output("Reasoning effort is unavailable for #{provider} #{model}.")
64
+ return
65
+ end
66
+
67
+ selected = @prompt.select("Reasoning effort", reasoning_choices(choices, conversation), title: "Reasoning")
68
+ return unless selected
69
+
70
+ effort, = choices.find { |_value, label| selected.to_s.downcase.start_with?(label.downcase) }
71
+ raise "Reasoning effort must be one of: #{choices.map(&:first).join(", ")}" unless effort
72
+
73
+ set_reasoning_effort(effort, conversation, provider: provider)
74
+ rescue StandardError => e
75
+ runtime_output("Reasoning error: #{e.message}")
76
+ end
77
+
78
+ def login_picker_available?
79
+ @prompt.respond_to?(:select)
80
+ end
81
+
82
+ def login_method_choices
83
+ ["API key", "Subscription / OAuth"]
84
+ end
85
+
86
+ def selected_login_method(selected)
87
+ case selected.to_s.downcase
88
+ when /\Aapi key\z/ then :api_key
89
+ when /\Asubscription \/ oauth\z/ then :oauth
90
+ end
91
+ end
92
+
93
+ def login_provider_prompt(method)
94
+ method == :api_key ? "Add an API key" : "Sign in with a subscription"
95
+ end
96
+
97
+ def login_provider_choices(method)
98
+ if method == :api_key
99
+ ProviderCatalog.api_key_providers.map(&:name)
100
+ else
101
+ ["Anthropic Claude", "ChatGPT", "GitHub Copilot"]
102
+ end
103
+ end
104
+
105
+ def selected_login_provider(selected)
106
+ value = selected.to_s.downcase
107
+ return "openai" if value == "openai" || value == "chatgpt"
108
+ return "anthropic" if value == "anthropic" || value == "anthropic claude"
109
+ return "copilot" if value == "github copilot"
110
+
111
+ ProviderCatalog.find_by_name(selected)&.id
112
+ end
113
+
114
+ def model_overlay_available?
115
+ @prompt.respond_to?(:select)
116
+ end
117
+
118
+ def settings_overlay_available?
119
+ @prompt.respond_to?(:select)
120
+ end
121
+
122
+ def update_overlay_settings(values)
123
+ settings = ConfigFiles.update_overlay_settings(values)
124
+ @prompt.update_overlay_settings(settings) if @prompt.respond_to?(:update_overlay_settings)
125
+ settings
126
+ end
127
+
128
+ def choose_overlay_setting(message, choices, values)
129
+ choice = @prompt.select(message, choices, title: "Settings")
130
+ return nil unless choice
131
+
132
+ values.find { |value| choice.to_s.downcase.start_with?(value) }
133
+ end
134
+
135
+ def normalized_available_models(conversation = current_footer_conversation, models: nil)
136
+ current_provider = conversation.provider || (@client.respond_to?(:current_provider) ? @client.current_provider : "Codex")
137
+ current_model = conversation.model || (@client.respond_to?(:current_model) ? @client.current_model : nil)
138
+ current_reasoning = conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : nil)
139
+ models ||= @client.respond_to?(:available_models) ? @client.available_models : []
140
+ ModelInfo.normalize_available(
141
+ models,
142
+ current_provider: current_provider,
143
+ current_model: current_model,
144
+ current_reasoning_effort: current_reasoning
145
+ )
146
+ end
147
+
148
+ def model_choices(models, conversation = current_footer_conversation)
149
+ current_provider = conversation.provider || current_model_provider
150
+ current_model = conversation.model || current_model_id
151
+ choices = models.map do |model|
152
+ label = "#{model[:provider]} #{model[:id]}"
153
+ label += " (current)" if model[:current]
154
+ label
155
+ end
156
+ choices.empty? ? ["#{current_provider} #{current_model} (current)"] : choices.uniq
157
+ end
158
+
159
+ def default_selectable_model?(model)
160
+ return true if model[:current]
161
+
162
+ capabilities = Array(model[:supportedParameters]).map { |value| value.to_s.downcase }
163
+ return true if capabilities.empty?
164
+
165
+ capabilities.any? do |capability|
166
+ ["tools", "toolchoice", "functioncalling", "generatecontent", "streamgeneratecontent"].include?(capability.delete("_-"))
167
+ end
168
+ end
169
+
170
+ def selected_model(selected, models)
171
+ text = selected.to_s.sub(/ \(current\)\z/, "").strip
172
+ known = models.find { |model| "#{model[:provider]} #{model[:id]}" == text }
173
+ return [known[:provider], known[:id]] if known
174
+
175
+ provider, model = text.split(/\s+/, 2)
176
+ known_provider = ["Codex", "Anthropic", "OpenRouter", "Copilot", "Local"].include?(provider) || ProviderCatalog.find_by_name(provider)
177
+ return [provider, model.strip] if known_provider && !model.to_s.strip.empty?
178
+
179
+ [current_model_provider, text]
180
+ end
181
+
182
+ def persist_model_selection(provider, model, conversation)
183
+ model = model.to_s.strip
184
+ raise "Model must be a non-empty string" if model.empty?
185
+
186
+ ConfigFiles.update_config(ModelInfo.config_values_for_selection(provider, model))
187
+ reload_client_config
188
+ refresh_conversation_runtime(conversation)
189
+ refresh_composer_status
190
+ end
191
+
192
+ REASONING_CONFIG_DEBOUNCE_SECONDS = 0.5
193
+
194
+ def cycle_reasoning(conversation = current_footer_conversation, direction: :next, persist: :immediate)
195
+ provider = conversation&.provider || current_model_provider
196
+ model = conversation&.model || current_model_id
197
+ choices = ModelInfo.reasoning_effort_choices(provider, model)
198
+ return false if choices.empty?
199
+
200
+ current = (pending_reasoning_effort(provider) || conversation&.reasoning_effort || current_reasoning_effort).to_s
201
+ current_index = choices.index { |effort, _label| effort == current }
202
+ current_index ||= direction == :previous ? 0 : -1
203
+ offset = direction == :previous ? -1 : 1
204
+ effort = choices[(current_index + offset) % choices.length].first
205
+ persist == :debounced ? apply_reasoning_effort(effort, conversation, provider: provider) : set_reasoning_effort(effort, conversation, provider: provider)
206
+ true
207
+ rescue StandardError => e
208
+ runtime_output("Reasoning error: #{e.message}")
209
+ false
210
+ end
211
+
212
+ def set_reasoning_effort(effort, conversation = nil, provider: nil)
213
+ @pending_reasoning_config_mutex.synchronize { @pending_reasoning_config = nil }
214
+ persist_reasoning_config(effort, provider: provider)
215
+ apply_reasoning_effort(effort, conversation, provider: provider, queue_config: false)
216
+ end
217
+
218
+ def apply_reasoning_effort(effort, conversation = nil, provider: nil, queue_config: true)
219
+ queue_reasoning_config(effort, provider: provider, conversation: conversation) if queue_config
220
+ if queue_config
221
+ update_conversation_reasoning_effort(conversation, effort)
222
+ refresh_composer_status
223
+ else
224
+ refresh_conversation_runtime(conversation, reasoning_effort: effort)
225
+ refresh_composer_status
226
+ end
227
+ end
228
+
229
+ def update_conversation_reasoning_effort(conversation, effort)
230
+ return unless conversation&.respond_to?(:update_runtime_context!)
231
+
232
+ conversation.update_runtime_context!(
233
+ provider: conversation.provider || current_model_provider,
234
+ model: conversation.model || current_model_id,
235
+ reasoning_effort: effort,
236
+ refresh: false
237
+ )
238
+ end
239
+
240
+ def pending_reasoning_effort(provider)
241
+ @pending_reasoning_config_mutex.synchronize do
242
+ pending = @pending_reasoning_config
243
+ return nil unless pending
244
+ return nil unless pending[:provider].to_s.downcase == provider.to_s.downcase
245
+
246
+ pending[:effort]
247
+ end
248
+ end
249
+
250
+ def queue_reasoning_config(effort, provider: nil, conversation: nil)
251
+ pending = {
252
+ effort: effort,
253
+ provider: provider || current_model_provider,
254
+ conversation: conversation,
255
+ deadline: Process.clock_gettime(Process::CLOCK_MONOTONIC) + REASONING_CONFIG_DEBOUNCE_SECONDS
256
+ }
257
+ @pending_reasoning_config_mutex.synchronize { @pending_reasoning_config = pending }
258
+ schedule_reasoning_config_flush
259
+ end
260
+
261
+ def schedule_reasoning_config_flush
262
+ return if @pending_reasoning_config_thread&.alive?
263
+
264
+ @pending_reasoning_config_thread = Thread.new do
265
+ loop do
266
+ sleep REASONING_CONFIG_DEBOUNCE_SECONDS
267
+ break if flush_pending_reasoning_config(force: false)
268
+ break unless @pending_reasoning_config_mutex.synchronize { @pending_reasoning_config }
269
+ end
270
+ rescue StandardError => e
271
+ runtime_output("Reasoning error: #{e.message}")
272
+ end
273
+ end
274
+
275
+ def flush_pending_reasoning_config(force: true, conversation: nil)
276
+ pending = nil
277
+ @pending_reasoning_config_mutex.synchronize do
278
+ pending = @pending_reasoning_config
279
+ return false unless pending
280
+
281
+ now = Process.clock_gettime(Process::CLOCK_MONOTONIC)
282
+ return false if !force && now < pending[:deadline].to_f
283
+
284
+ @pending_reasoning_config = nil
285
+ end
286
+ persist_reasoning_config(pending[:effort], provider: pending[:provider])
287
+ conversation ||= pending[:conversation]
288
+ if conversation&.reasoning_effort.to_s == pending[:effort].to_s
289
+ refresh_conversation_runtime(conversation, reasoning_effort: pending[:effort])
290
+ conversation.persist_runtime_context! if conversation.respond_to?(:persist_runtime_context!)
291
+ end
292
+ true
293
+ end
294
+
295
+ def persist_reasoning_config(effort, provider: nil)
296
+ ConfigFiles.update_config(ModelInfo.reasoning_config_key_for_provider(provider || current_model_provider) => effort)
297
+ reload_client_config
298
+ end
299
+
300
+ def reasoning_choices(choices, conversation = current_footer_conversation)
301
+ current = (conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : ModelInfo::DEFAULT_REASONING_EFFORT)).to_s
302
+ choices.map do |effort, label|
303
+ text = label.dup
304
+ text += " (current)" if current == effort
305
+ text
306
+ end
307
+ end
308
+
309
+ def overlay_alignment_choices(settings)
310
+ ConfigFiles::OVERLAY_ALIGNMENTS.map do |alignment|
311
+ label = alignment.capitalize
312
+ label += " (current)" if settings["alignment"] == alignment
313
+ label
314
+ end
315
+ end
316
+
317
+ def overlay_width_choices(settings)
318
+ ConfigFiles::OVERLAY_WIDTHS.map do |width|
319
+ label = width.capitalize
320
+ label += " (current)" if settings["width"] == width
321
+ label
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
327
+ end