kward 0.74.0 → 0.75.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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/Gemfile.lock +2 -2
  4. data/README.md +4 -2
  5. data/Rakefile +18 -0
  6. data/doc/configuration.md +56 -3
  7. data/doc/extensibility.md +6 -19
  8. data/doc/getting-started.md +1 -1
  9. data/doc/lifecycle-hooks.md +424 -0
  10. data/doc/memory.md +1 -1
  11. data/doc/personas.md +2 -2
  12. data/doc/plugins.md +1 -1
  13. data/doc/releasing.md +7 -15
  14. data/doc/rpc.md +108 -16
  15. data/doc/session-management.md +9 -9
  16. data/doc/skills.md +199 -0
  17. data/doc/usage.md +7 -7
  18. data/lib/kward/agent.rb +72 -22
  19. data/lib/kward/auth/anthropic_oauth.rb +5 -87
  20. data/lib/kward/auth/oauth_helpers.rb +105 -0
  21. data/lib/kward/auth/openai_oauth.rb +4 -86
  22. data/lib/kward/cli/commands.rb +9 -2
  23. data/lib/kward/cli/compaction.rb +7 -0
  24. data/lib/kward/cli/git.rb +30 -3
  25. data/lib/kward/cli/hook_commands.rb +258 -0
  26. data/lib/kward/cli/memory_commands.rb +1 -5
  27. data/lib/kward/cli/plugins.rb +41 -1
  28. data/lib/kward/cli/prompt_interface.rb +32 -3
  29. data/lib/kward/cli/rendering.rb +1 -1
  30. data/lib/kward/cli/runtime_helpers.rb +44 -5
  31. data/lib/kward/cli/sessions.rb +13 -0
  32. data/lib/kward/cli/settings.rb +226 -152
  33. data/lib/kward/cli/slash_commands.rb +81 -42
  34. data/lib/kward/cli/tabs.rb +26 -6
  35. data/lib/kward/cli.rb +26 -3
  36. data/lib/kward/cli_transcript_formatter.rb +0 -7
  37. data/lib/kward/compaction/token_estimator.rb +112 -0
  38. data/lib/kward/compactor.rb +42 -116
  39. data/lib/kward/config_files.rb +117 -25
  40. data/lib/kward/deep_copy.rb +42 -0
  41. data/lib/kward/diff_view_mode.rb +36 -0
  42. data/lib/kward/events.rb +1 -0
  43. data/lib/kward/export_path.rb +2 -6
  44. data/lib/kward/frontmatter.rb +43 -0
  45. data/lib/kward/hooks/audit_log.rb +121 -0
  46. data/lib/kward/hooks/catalog.rb +110 -0
  47. data/lib/kward/hooks/command_handler.rb +66 -0
  48. data/lib/kward/hooks/config_loader.rb +104 -0
  49. data/lib/kward/hooks/decision.rb +102 -0
  50. data/lib/kward/hooks/event.rb +60 -0
  51. data/lib/kward/hooks/http_handler.rb +72 -0
  52. data/lib/kward/hooks/manager.rb +158 -0
  53. data/lib/kward/hooks/matcher.rb +75 -0
  54. data/lib/kward/hooks.rb +16 -0
  55. data/lib/kward/markdown_transcript.rb +4 -3
  56. data/lib/kward/mcp/client.rb +1 -1
  57. data/lib/kward/mcp/server_config.rb +7 -3
  58. data/lib/kward/mcp/stdio_transport.rb +7 -1
  59. data/lib/kward/memory/turn_context.rb +19 -0
  60. data/lib/kward/message_access.rb +2 -2
  61. data/lib/kward/message_text.rb +19 -0
  62. data/lib/kward/model/client.rb +13 -45
  63. data/lib/kward/model/context_usage.rb +10 -4
  64. data/lib/kward/model/copilot_models.rb +44 -0
  65. data/lib/kward/model/model_info.rb +24 -0
  66. data/lib/kward/model/payloads.rb +15 -17
  67. data/lib/kward/model/stream_parser.rb +178 -13
  68. data/lib/kward/pan/server.rb +51 -2
  69. data/lib/kward/path_guard.rb +13 -0
  70. data/lib/kward/plugin_registry.rb +86 -25
  71. data/lib/kward/prompt_interface/editor/controller.rb +60 -1
  72. data/lib/kward/prompt_interface/editor/renderer.rb +11 -0
  73. data/lib/kward/prompt_interface/editor/state.rb +5 -1
  74. data/lib/kward/prompt_interface/selection_prompt.rb +26 -6
  75. data/lib/kward/prompt_interface.rb +4 -1
  76. data/lib/kward/prompts/commands.rb +3 -3
  77. data/lib/kward/prompts.rb +7 -7
  78. data/lib/kward/rpc/mcp_status.rb +50 -0
  79. data/lib/kward/rpc/prompt_bridge.rb +38 -4
  80. data/lib/kward/rpc/runtime_payloads.rb +1 -1
  81. data/lib/kward/rpc/server.rb +94 -14
  82. data/lib/kward/rpc/session_manager.rb +258 -33
  83. data/lib/kward/rpc/session_metrics.rb +1 -1
  84. data/lib/kward/rpc/session_tree_rows.rb +13 -64
  85. data/lib/kward/rpc/tool_event_normalizer.rb +16 -2
  86. data/lib/kward/rpc/transcript_normalizer.rb +9 -10
  87. data/lib/kward/rpc/turn_context.rb +109 -0
  88. data/lib/kward/session_store.rb +6 -20
  89. data/lib/kward/session_tree_nodes.rb +98 -18
  90. data/lib/kward/session_tree_renderer.rb +6 -61
  91. data/lib/kward/skills/registry.rb +109 -17
  92. data/lib/kward/tools/mcp_tool.rb +4 -0
  93. data/lib/kward/tools/read_skill.rb +9 -4
  94. data/lib/kward/tools/registry.rb +266 -8
  95. data/lib/kward/tools/search/code.rb +4 -22
  96. data/lib/kward/tools/search/web.rb +28 -2
  97. data/lib/kward/update_check.rb +117 -0
  98. data/lib/kward/version.rb +1 -1
  99. data/lib/kward/workers/manager.rb +43 -3
  100. data/lib/kward/workspace.rb +3 -1
  101. data/templates/default/fulldoc/html/css/kward.css +22 -0
  102. data/templates/default/fulldoc/html/js/kward.js +2 -0
  103. data/templates/default/kward_navigation.rb +4 -2
  104. data/templates/default/layout/html/setup.rb +2 -0
  105. metadata +25 -1
@@ -12,11 +12,14 @@ module Kward
12
12
  return
13
13
  end
14
14
 
15
+ initial_index = 0
15
16
  loop do
16
- selected = @prompt.select("Settings category", settings_category_choices, title: "Settings")
17
+ choices = settings_category_choices
18
+ selected, selected_index = select_settings_menu_item("Settings category", choices, initial_index)
17
19
  category = selected_settings_category(selected)
18
20
  break unless category
19
21
 
22
+ initial_index = selected_index if selected_index
20
23
  break if category == "done"
21
24
 
22
25
  handle_settings_category(category, conversation)
@@ -83,14 +86,22 @@ module Kward
83
86
  end
84
87
 
85
88
  def configure_model_settings(conversation)
86
- selected = @prompt.select("Model & Reasoning", ["Provider", "Default model", "Reasoning effort", "Back"], title: "Settings")
87
- case selected.to_s.downcase
88
- when /\Aprovider/
89
- configure_provider(conversation)
90
- when /\Adefault model/
91
- configure_model(conversation)
92
- when /\Areasoning effort/
93
- configure_reasoning(conversation)
89
+ initial_index = 0
90
+ loop do
91
+ selected, selected_index = select_settings_menu_item("Model & Reasoning", ["Provider", "Default model", "Reasoning effort", "Back"], initial_index)
92
+ break unless selected
93
+
94
+ initial_index = selected_index if selected_index
95
+ case selected.to_s.downcase
96
+ when /\Aprovider/
97
+ configure_provider(conversation)
98
+ when /\Adefault model/
99
+ configure_model(conversation)
100
+ when /\Areasoning effort/
101
+ configure_reasoning(conversation)
102
+ else
103
+ break
104
+ end
94
105
  end
95
106
  end
96
107
 
@@ -125,22 +136,30 @@ module Kward
125
136
  end
126
137
 
127
138
  def configure_account_settings
128
- selected = @prompt.select("Accounts", account_setting_choices, title: "Settings")
129
- case selected.to_s.downcase
130
- when /\Aopenai/
131
- login(provider: "openai")
132
- reload_client_config
133
- when /\Aanthropic/, /\Aclaude/
134
- login(provider: "anthropic")
135
- reload_client_config
136
- when /\Agithub/
137
- login(provider: "github")
138
- reload_client_config
139
- when /\Aopenrouter/
140
- login(provider: "openrouter")
141
- reload_client_config
142
- when /\Astatus/
143
- print_auth_status
139
+ initial_index = 0
140
+ loop do
141
+ selected, selected_index = select_settings_menu_item("Accounts", account_setting_choices, initial_index)
142
+ break unless selected
143
+
144
+ initial_index = selected_index if selected_index
145
+ case selected.to_s.downcase
146
+ when /\Aopenai/
147
+ login(provider: "openai")
148
+ reload_client_config
149
+ when /\Aanthropic/, /\Aclaude/
150
+ login(provider: "anthropic")
151
+ reload_client_config
152
+ when /\Agithub/
153
+ login(provider: "github")
154
+ reload_client_config
155
+ when /\Aopenrouter/
156
+ login(provider: "openrouter")
157
+ reload_client_config
158
+ when /\Astatus/
159
+ print_auth_status
160
+ else
161
+ break
162
+ end
144
163
  end
145
164
  end
146
165
 
@@ -163,17 +182,25 @@ module Kward
163
182
  end
164
183
 
165
184
  def configure_memory_settings(conversation)
166
- selected = @prompt.select("Memory", memory_setting_choices, title: "Settings")
167
- case selected.to_s.downcase
168
- when /\Aenable memory/, /\Adisable memory/
169
- set_memory_enabled(!memory_enabled?)
170
- conversation&.refresh_system_message!
171
- runtime_output("Memory #{memory_enabled? ? "enabled" : "disabled"}.")
172
- when /\Aenable auto-summary/, /\Adisable auto-summary/
173
- set_memory_auto_summary_enabled(!memory_auto_summary_enabled?)
174
- runtime_output("Memory auto-summary #{memory_auto_summary_enabled? ? "enabled" : "disabled"}.")
175
- when /\Amanage/
176
- runtime_output("Use /memory enable|disable|auto-summary enable|disable|core <text>|add <text>|list|forget <id>|promote <id>|relax <id>|inspect|why|summarize.")
185
+ initial_index = 0
186
+ loop do
187
+ selected, selected_index = select_settings_menu_item("Memory", memory_setting_choices, initial_index)
188
+ break unless selected
189
+
190
+ initial_index = selected_index if selected_index
191
+ case selected.to_s.downcase
192
+ when /\Aenable memory/, /\Adisable memory/
193
+ set_config_flag("memory", "enabled", !memory_enabled?)
194
+ conversation&.refresh_system_message!
195
+ runtime_output("Memory #{memory_enabled? ? "enabled" : "disabled"}.")
196
+ when /\Aenable auto-summary/, /\Adisable auto-summary/
197
+ set_config_flag("memory", "auto_summary", !memory_auto_summary_enabled?)
198
+ runtime_output("Memory auto-summary #{memory_auto_summary_enabled? ? "enabled" : "disabled"}.")
199
+ when /\Amanage/
200
+ runtime_output("Use /memory enable|disable|auto-summary enable|disable|core <text>|add <text>|list|forget <id>|promote <id>|relax <id>|inspect|why|summarize.")
201
+ else
202
+ break
203
+ end
177
204
  end
178
205
  end
179
206
 
@@ -196,50 +223,52 @@ module Kward
196
223
  memory.is_a?(Hash) && memory["auto_summary"] == true
197
224
  end
198
225
 
199
- def set_memory_enabled(enabled)
200
- update_nested_config("memory", "enabled" => enabled)
201
- end
202
-
203
- def set_memory_auto_summary_enabled(enabled)
204
- update_nested_config("memory", "auto_summary" => enabled)
205
- end
206
-
207
226
  def configure_interface_settings
208
- selected = @prompt.select("Interface", interface_setting_choices, title: "Settings")
209
- case selected.to_s.downcase
210
- when /\Aoverlay alignment/
211
- settings = ConfigFiles.overlay_settings
212
- alignment = choose_overlay_setting("Overlay alignment", overlay_alignment_choices(settings), ConfigFiles::OVERLAY_ALIGNMENTS)
213
- return unless alignment
214
-
215
- @prompt.update_overlay_settings(ConfigFiles.update_overlay_settings("alignment" => alignment))
216
- when /\Aoverlay width/
217
- settings = ConfigFiles.overlay_settings
218
- width = choose_overlay_setting("Overlay width", overlay_width_choices(settings), ConfigFiles::OVERLAY_WIDTHS)
219
- return unless width
220
-
221
- @prompt.update_overlay_settings(ConfigFiles.update_overlay_settings("width" => width))
222
- when /\Ashow busy help/, /\Ahide busy help/
223
- set_composer_busy_help(!composer_busy_help?)
224
- runtime_output("Busy help #{composer_busy_help? ? "enabled" : "disabled"}. Restart the TUI to apply this setting.")
225
- when /\Atab keybindings/
226
- configure_tab_keybindings
227
- when /\Aeditor mode/
228
- configure_editor_mode
229
- when /\Aeditor line numbers/
230
- configure_editor_line_numbers
231
- when /\Aenable auto-close pairs/, /\Adisable auto-close pairs/
232
- set_editor_auto_close_pairs_enabled(!editor_auto_close_pairs_enabled?)
233
- runtime_output("Editor auto-close pairs #{editor_auto_close_pairs_enabled? ? "enabled" : "disabled"}.")
234
- when /\Aenable soft-wrap/, /\Adisable soft-wrap/
235
- set_editor_soft_wrap_enabled(!editor_soft_wrap_enabled?)
236
- runtime_output("Editor soft-wrap #{editor_soft_wrap_enabled? ? "enabled" : "disabled"}.")
237
- when /\Aenable bar cursor/, /\Adisable bar cursor/
238
- set_editor_bar_cursor_enabled(!editor_bar_cursor_enabled?)
239
- runtime_output("Editor bar cursor #{editor_bar_cursor_enabled? ? "enabled" : "disabled"}.")
240
- when /\Aenable session auto-resume/, /\Adisable session auto-resume/
241
- set_session_auto_resume_enabled(!session_auto_resume_enabled?)
242
- runtime_output("Session auto-resume #{session_auto_resume_enabled? ? "enabled" : "disabled"}.")
227
+ initial_index = 0
228
+ loop do
229
+ selected, selected_index = select_settings_menu_item("Interface", interface_setting_choices, initial_index)
230
+ break unless selected
231
+
232
+ initial_index = selected_index if selected_index
233
+ case selected.to_s.downcase
234
+ when /\Aoverlay alignment/
235
+ settings = ConfigFiles.overlay_settings
236
+ alignment = choose_overlay_setting("Overlay alignment", overlay_alignment_choices(settings), ConfigFiles::OVERLAY_ALIGNMENTS)
237
+ next unless alignment
238
+
239
+ update_overlay_settings("alignment" => alignment)
240
+ when /\Aoverlay width/
241
+ settings = ConfigFiles.overlay_settings
242
+ width = choose_overlay_setting("Overlay width", overlay_width_choices(settings), ConfigFiles::OVERLAY_WIDTHS)
243
+ next unless width
244
+
245
+ update_overlay_settings("width" => width)
246
+ when /\Ashow busy help/, /\Ahide busy help/
247
+ set_composer_busy_help(!composer_busy_help?)
248
+ runtime_output("Busy help #{composer_busy_help? ? "enabled" : "disabled"}. Restart the TUI to apply this setting.")
249
+ when /\Atab keybindings/
250
+ configure_tab_keybindings
251
+ when /\Aeditor mode/
252
+ configure_editor_mode
253
+ when /\Aeditor line numbers/
254
+ configure_editor_line_numbers
255
+ when /\Adiff view/
256
+ configure_diff_view
257
+ when /\Aenable auto-close pairs/, /\Adisable auto-close pairs/
258
+ set_config_flag("editor", "auto_close_pairs", !editor_auto_close_pairs_enabled?)
259
+ runtime_output("Editor auto-close pairs #{editor_auto_close_pairs_enabled? ? "enabled" : "disabled"}.")
260
+ when /\Aenable soft-wrap/, /\Adisable soft-wrap/
261
+ set_config_flag("editor", "soft_wrap", !editor_soft_wrap_enabled?)
262
+ runtime_output("Editor soft-wrap #{editor_soft_wrap_enabled? ? "enabled" : "disabled"}.")
263
+ when /\Aenable bar cursor/, /\Adisable bar cursor/
264
+ set_config_flag("editor", "bar_cursor", !editor_bar_cursor_enabled?)
265
+ runtime_output("Editor bar cursor #{editor_bar_cursor_enabled? ? "enabled" : "disabled"}.")
266
+ when /\Aenable session auto-resume/, /\Adisable session auto-resume/
267
+ set_config_flag("sessions", "auto_resume", !session_auto_resume_enabled?)
268
+ runtime_output("Session auto-resume #{session_auto_resume_enabled? ? "enabled" : "disabled"}.")
269
+ else
270
+ break
271
+ end
243
272
  end
244
273
  end
245
274
 
@@ -252,6 +281,7 @@ module Kward
252
281
  "Tab keybindings (#{composer_tab_keybindings})",
253
282
  "Editor mode (#{editor_mode})",
254
283
  "Editor line numbers (#{editor_line_numbers})",
284
+ "Diff view (#{diff_view_label(diff_view)})",
255
285
  "#{editor_auto_close_pairs_enabled? ? "Disable" : "Enable"} auto-close pairs (currently #{on_off(editor_auto_close_pairs_enabled?)})",
256
286
  "#{editor_soft_wrap_enabled? ? "Disable" : "Enable"} soft-wrap (currently #{on_off(editor_soft_wrap_enabled?)})",
257
287
  "#{editor_bar_cursor_enabled? ? "Disable" : "Enable"} bar cursor (currently #{on_off(editor_bar_cursor_enabled?)})",
@@ -318,28 +348,41 @@ module Kward
318
348
  %w[absolute relative].map { |value| value == current ? "#{value} (current)" : value }
319
349
  end
320
350
 
321
- def editor_auto_close_pairs_enabled?
322
- ConfigFiles.editor_auto_close_pairs?(safely_read_config.to_h)
351
+ def diff_view
352
+ ConfigFiles.diff_view(safely_read_config.to_h)
323
353
  end
324
354
 
325
- def set_editor_auto_close_pairs_enabled(enabled)
326
- update_nested_config("editor", "auto_close_pairs" => enabled)
355
+ def configure_diff_view
356
+ selected = @prompt.select("Diff view", diff_view_choices, title: "Settings")
357
+ value = selected.to_s.split.first.to_s.downcase.tr("-", "_")
358
+ return unless Kward::DiffViewMode::MODES.include?(value)
359
+
360
+ update_nested_config("editor", "diff_view" => value)
361
+ runtime_output("Diff view set to #{diff_view_label(value)}.")
327
362
  end
328
363
 
329
- def editor_soft_wrap_enabled?
330
- ConfigFiles.editor_soft_wrap?(safely_read_config.to_h)
364
+ def diff_view_choices
365
+ current = diff_view
366
+ Kward::DiffViewMode::MODES.map do |value|
367
+ label = diff_view_label(value)
368
+ value == current ? "#{label} (current)" : label
369
+ end
331
370
  end
332
371
 
333
- def set_editor_soft_wrap_enabled(enabled)
334
- update_nested_config("editor", "soft_wrap" => enabled)
372
+ def diff_view_label(value)
373
+ Kward::DiffViewMode.label(value)
335
374
  end
336
375
 
337
- def editor_bar_cursor_enabled?
338
- ConfigFiles.editor_bar_cursor?(safely_read_config.to_h)
376
+ def editor_auto_close_pairs_enabled?
377
+ ConfigFiles.editor_auto_close_pairs?(safely_read_config.to_h)
339
378
  end
340
379
 
341
- def set_editor_bar_cursor_enabled(enabled)
342
- update_nested_config("editor", "bar_cursor" => enabled)
380
+ def editor_soft_wrap_enabled?
381
+ ConfigFiles.editor_soft_wrap?(safely_read_config.to_h)
382
+ end
383
+
384
+ def editor_bar_cursor_enabled?
385
+ ConfigFiles.editor_bar_cursor?(safely_read_config.to_h)
343
386
  end
344
387
 
345
388
  def session_auto_resume_enabled?
@@ -347,27 +390,34 @@ module Kward
347
390
  end
348
391
 
349
392
  def set_composer_busy_help(enabled)
350
- update_nested_config("composer", "busy_help" => enabled)
351
- end
352
-
353
- def set_session_auto_resume_enabled(enabled)
354
- update_nested_config("sessions", "auto_resume" => enabled)
393
+ set_config_flag("composer", "busy_help", enabled)
355
394
  end
356
395
 
357
396
  def configure_tools_settings
358
- selected = @prompt.select("Tools & Search", tools_setting_choices, title: "Settings")
359
- case selected.to_s.downcase
360
- when /\Aenable web search/, /\Adisable web search/
361
- set_web_search_enabled(!web_search_enabled?)
362
- runtime_output("Web search #{web_search_enabled? ? "enabled" : "disabled"}.")
363
- when /\Aweb search provider/
364
- configure_web_search_provider
365
- when /\Aallow model-provider/, /\Adisallow model-provider/
366
- set_web_search_allow_model_providers(!web_search_allow_model_providers?)
367
- runtime_output("Model-provider web search #{web_search_allow_model_providers? ? "enabled" : "disabled"}.")
368
- when /\Aenable workspace guardrails/, /\Adisable workspace guardrails/
369
- set_workspace_guardrails_enabled(!workspace_guardrails_enabled?)
370
- runtime_output("Workspace guardrails #{workspace_guardrails_enabled? ? "enabled" : "disabled"}.")
397
+ initial_index = 0
398
+ loop do
399
+ selected, selected_index = select_settings_menu_item("Tools & Search", tools_setting_choices, initial_index)
400
+ break unless selected
401
+
402
+ initial_index = selected_index if selected_index
403
+ case selected.to_s.downcase
404
+ when /\Aenable web search/, /\Adisable web search/
405
+ set_config_flag("web_search", "enabled", !web_search_enabled?)
406
+ runtime_output("Web search #{web_search_enabled? ? "enabled" : "disabled"}.")
407
+ when /\Aweb search provider/
408
+ configure_web_search_provider
409
+ when /\Aallow model-provider/, /\Adisallow model-provider/
410
+ set_config_flag("web_search", "allow_model_providers", !web_search_allow_model_providers?)
411
+ runtime_output("Model-provider web search #{web_search_allow_model_providers? ? "enabled" : "disabled"}.")
412
+ when /\Atrust project skills/, /\Auntrust project skills/
413
+ set_config_flag("skills", "trust_project", !project_skills_trusted?)
414
+ runtime_output("Project skills #{project_skills_trusted? ? "trusted" : "untrusted"}.")
415
+ when /\Aenable workspace guardrails/, /\Adisable workspace guardrails/
416
+ set_config_flag("tools", "workspace_guardrails", !workspace_guardrails_enabled?)
417
+ runtime_output("Workspace guardrails #{workspace_guardrails_enabled? ? "enabled" : "disabled"}.")
418
+ else
419
+ break
420
+ end
371
421
  end
372
422
  end
373
423
 
@@ -376,6 +426,7 @@ module Kward
376
426
  "#{web_search_enabled? ? "Disable" : "Enable"} web search (currently #{on_off(web_search_enabled?)})",
377
427
  "Web search provider (#{web_search_provider})",
378
428
  "#{web_search_allow_model_providers? ? "Disallow" : "Allow"} model-provider web search (currently #{on_off(web_search_allow_model_providers?)})",
429
+ "#{project_skills_trusted? ? "Untrust" : "Trust"} project skills (currently #{on_off(project_skills_trusted?)})",
379
430
  "#{workspace_guardrails_enabled? ? "Disable" : "Enable"} workspace guardrails (currently #{on_off(workspace_guardrails_enabled?)})",
380
431
  "Back"
381
432
  ]
@@ -406,26 +457,26 @@ module Kward
406
457
  web_search_config["allow_model_providers"] == true
407
458
  end
408
459
 
409
- def set_web_search_enabled(enabled)
410
- update_nested_config("web_search", "enabled" => enabled)
411
- end
412
-
413
- def set_web_search_allow_model_providers(enabled)
414
- update_nested_config("web_search", "allow_model_providers" => enabled)
415
- end
416
-
417
- def set_workspace_guardrails_enabled(enabled)
418
- update_nested_config("tools", "workspace_guardrails" => enabled)
460
+ def project_skills_trusted?
461
+ ConfigFiles.project_skills_trusted?(safely_read_config.to_h)
419
462
  end
420
463
 
421
464
  def configure_context_settings
422
- selected = @prompt.select("Context & Compaction", context_setting_choices, title: "Settings")
423
- case selected.to_s.downcase
424
- when /\Aenable auto-compaction/, /\Adisable auto-compaction/
425
- set_compaction_enabled(!compaction_enabled?)
426
- runtime_output("Auto-compaction #{compaction_enabled? ? "enabled" : "disabled"}.")
427
- else
428
- runtime_output(auto_compaction_status_line) if selected.to_s.downcase.start_with?("status")
465
+ initial_index = 0
466
+ loop do
467
+ selected, selected_index = select_settings_menu_item("Context & Compaction", context_setting_choices, initial_index)
468
+ break unless selected
469
+
470
+ initial_index = selected_index if selected_index
471
+ case selected.to_s.downcase
472
+ when /\Aenable auto-compaction/, /\Adisable auto-compaction/
473
+ set_config_flag("compaction", "enabled", !compaction_enabled?)
474
+ runtime_output("Auto-compaction #{compaction_enabled? ? "enabled" : "disabled"}.")
475
+ when /\Astatus/
476
+ runtime_output(auto_compaction_status_line)
477
+ else
478
+ break
479
+ end
429
480
  end
430
481
  end
431
482
 
@@ -441,17 +492,21 @@ module Kward
441
492
  Kward::Compaction::Settings.from_config(safely_read_config.to_h).enabled
442
493
  end
443
494
 
444
- def set_compaction_enabled(enabled)
445
- update_nested_config("compaction", "enabled" => enabled)
446
- end
447
-
448
495
  def configure_personalization_settings(conversation)
449
- selected = @prompt.select("Personalization", personalization_setting_choices(conversation), title: "Settings")
450
- case selected.to_s.downcase
451
- when /\Adefault persona/
452
- configure_default_persona(conversation)
453
- when /\Aactive instructions/
454
- show_active_instructions_summary(conversation)
496
+ initial_index = 0
497
+ loop do
498
+ selected, selected_index = select_settings_menu_item("Personalization", personalization_setting_choices(conversation), initial_index)
499
+ break unless selected
500
+
501
+ initial_index = selected_index if selected_index
502
+ case selected.to_s.downcase
503
+ when /\Adefault persona/
504
+ configure_default_persona(conversation)
505
+ when /\Aactive instructions/
506
+ show_active_instructions_summary(conversation)
507
+ else
508
+ break
509
+ end
455
510
  end
456
511
  end
457
512
 
@@ -500,12 +555,18 @@ module Kward
500
555
  end
501
556
 
502
557
  def configure_logging_settings
503
- selected = @prompt.select("Logging", logging_setting_choices, title: "Settings")
504
- key = logging_key_for_choice(selected)
505
- return unless key
558
+ initial_index = 0
559
+ loop do
560
+ selected, selected_index = select_settings_menu_item("Logging", logging_setting_choices, initial_index)
561
+ break unless selected
562
+
563
+ initial_index = selected_index if selected_index
564
+ key = logging_key_for_choice(selected)
565
+ break unless key
506
566
 
507
- set_logging_value(key, !logging_enabled?(key))
508
- runtime_output("Logging #{key.tr("_", " ")} #{logging_enabled?(key) ? "enabled" : "disabled"}.")
567
+ set_logging_value(key, !logging_enabled?(key))
568
+ runtime_output("Logging #{key.tr("_", " ")} #{logging_enabled?(key) ? "enabled" : "disabled"}.")
569
+ end
509
570
  end
510
571
 
511
572
  def logging_setting_choices
@@ -536,7 +597,7 @@ module Kward
536
597
  end
537
598
 
538
599
  def set_logging_value(key, value)
539
- update_nested_config("logging", key => value)
600
+ set_config_flag("logging", key, value)
540
601
  end
541
602
 
542
603
  def show_advanced_settings
@@ -557,6 +618,15 @@ module Kward
557
618
  ConfigFiles.update_nested_config(section, values)
558
619
  end
559
620
 
621
+ def set_config_flag(section, key, enabled)
622
+ update_nested_config(section, key => enabled)
623
+ end
624
+
625
+ def select_settings_menu_item(message, choices, initial_index)
626
+ selected = @prompt.select(message, choices, title: "Settings", initial_index: initial_index)
627
+ [selected, choices.index(selected)]
628
+ end
629
+
560
630
  def on_off(value)
561
631
  value ? "on" : "off"
562
632
  end
@@ -652,7 +722,13 @@ module Kward
652
722
  end
653
723
 
654
724
  def settings_overlay_available?
655
- @prompt.respond_to?(:select) && @prompt.respond_to?(:update_overlay_settings)
725
+ @prompt.respond_to?(:select)
726
+ end
727
+
728
+ def update_overlay_settings(values)
729
+ settings = ConfigFiles.update_overlay_settings(values)
730
+ @prompt.update_overlay_settings(settings) if @prompt.respond_to?(:update_overlay_settings)
731
+ settings
656
732
  end
657
733
 
658
734
  def choose_overlay_setting(message, choices, values)
@@ -666,15 +742,13 @@ module Kward
666
742
  current_provider = conversation.provider || (@client.respond_to?(:current_provider) ? @client.current_provider : "Codex")
667
743
  current_model = conversation.model || (@client.respond_to?(:current_model) ? @client.current_model : nil)
668
744
  current_reasoning = conversation.reasoning_effort || (@client.respond_to?(:current_reasoning_effort) ? @client.current_reasoning_effort : nil)
669
- models = @client.respond_to?(:available_models) ? Array(@client.available_models) : []
670
- models.map do |model|
671
- ModelInfo.normalize(
672
- model,
673
- current_provider: current_provider,
674
- current_model: current_model,
675
- current_reasoning_effort: current_reasoning
676
- )
677
- end
745
+ models = @client.respond_to?(:available_models) ? @client.available_models : []
746
+ ModelInfo.normalize_available(
747
+ models,
748
+ current_provider: current_provider,
749
+ current_model: current_model,
750
+ current_reasoning_effort: current_reasoning
751
+ )
678
752
  end
679
753
 
680
754
  def model_choices(models, conversation = current_footer_conversation)