kward 0.71.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 (116) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -1
  3. data/Gemfile.lock +2 -2
  4. data/README.md +4 -0
  5. data/doc/agent-tools.md +15 -6
  6. data/doc/authentication.md +22 -1
  7. data/doc/code-search.md +42 -2
  8. data/doc/configuration.md +106 -3
  9. data/doc/context-budgeting.md +136 -0
  10. data/doc/context-tools.md +16 -3
  11. data/doc/editor.md +394 -0
  12. data/doc/extensibility.md +16 -7
  13. data/doc/files.md +100 -0
  14. data/doc/getting-started.md +25 -18
  15. data/doc/git.md +122 -0
  16. data/doc/memory.md +24 -4
  17. data/doc/personas.md +34 -5
  18. data/doc/plugins.md +72 -1
  19. data/doc/releasing.md +37 -9
  20. data/doc/rpc.md +74 -4
  21. data/doc/session-management.md +35 -1
  22. data/doc/shell.md +286 -0
  23. data/doc/tabs.md +122 -0
  24. data/doc/troubleshooting.md +77 -1
  25. data/doc/usage.md +53 -7
  26. data/doc/web-search.md +12 -4
  27. data/doc/workspace-tools.md +51 -12
  28. data/examples/plugins/space_invaders.rb +377 -0
  29. data/lib/kward/agent.rb +1 -1
  30. data/lib/kward/cli/commands.rb +33 -2
  31. data/lib/kward/cli/git.rb +150 -0
  32. data/lib/kward/cli/interactive_turn.rb +73 -9
  33. data/lib/kward/cli/plugins.rb +54 -4
  34. data/lib/kward/cli/prompt_interface.rb +32 -1
  35. data/lib/kward/cli/runtime_helpers.rb +133 -3
  36. data/lib/kward/cli/sessions.rb +2 -2
  37. data/lib/kward/cli/settings.rb +218 -9
  38. data/lib/kward/cli/slash_commands.rb +415 -2
  39. data/lib/kward/cli/tabs.rb +695 -0
  40. data/lib/kward/cli.rb +158 -26
  41. data/lib/kward/config_files.rb +123 -1
  42. data/lib/kward/context_budget_meter.rb +44 -0
  43. data/lib/kward/conversation.rb +12 -4
  44. data/lib/kward/editor_mode.rb +25 -0
  45. data/lib/kward/ekwsh.rb +362 -0
  46. data/lib/kward/plugin_registry.rb +61 -0
  47. data/lib/kward/project_files.rb +52 -0
  48. data/lib/kward/prompt_history.rb +82 -0
  49. data/lib/kward/prompt_interface/composer_controller.rb +69 -1
  50. data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
  51. data/lib/kward/prompt_interface/composer_state.rb +96 -27
  52. data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
  53. data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
  54. data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
  55. data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
  56. data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
  57. data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
  58. data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
  59. data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
  60. data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
  61. data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
  62. data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
  63. data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
  64. data/lib/kward/prompt_interface/editor/search.rb +76 -0
  65. data/lib/kward/prompt_interface/editor/selections.rb +120 -0
  66. data/lib/kward/prompt_interface/editor/state.rb +1249 -0
  67. data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
  68. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
  69. data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
  70. data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
  71. data/lib/kward/prompt_interface/file_overlay.rb +211 -0
  72. data/lib/kward/prompt_interface/git_prompt.rb +299 -0
  73. data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
  74. data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
  75. data/lib/kward/prompt_interface/interactive/state.rb +62 -0
  76. data/lib/kward/prompt_interface/key_handler.rb +387 -35
  77. data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
  78. data/lib/kward/prompt_interface/project_browser.rb +524 -0
  79. data/lib/kward/prompt_interface/question_prompt.rb +98 -50
  80. data/lib/kward/prompt_interface/runtime_state.rb +43 -0
  81. data/lib/kward/prompt_interface/screen.rb +16 -0
  82. data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
  83. data/lib/kward/prompt_interface/stream_state.rb +7 -0
  84. data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
  85. data/lib/kward/prompt_interface.rb +286 -8
  86. data/lib/kward/prompts/commands.rb +5 -0
  87. data/lib/kward/prompts.rb +2 -0
  88. data/lib/kward/rpc/server.rb +42 -3
  89. data/lib/kward/rpc/session_manager.rb +35 -47
  90. data/lib/kward/rpc/session_tree_rows.rb +9 -115
  91. data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
  92. data/lib/kward/session_store.rb +44 -0
  93. data/lib/kward/session_tree_nodes.rb +136 -0
  94. data/lib/kward/session_tree_renderer.rb +9 -131
  95. data/lib/kward/tab_store.rb +47 -0
  96. data/lib/kward/text_boundary.rb +25 -0
  97. data/lib/kward/tools/context_budget_stats.rb +54 -0
  98. data/lib/kward/tools/context_for_task.rb +202 -0
  99. data/lib/kward/tools/read_file.rb +8 -4
  100. data/lib/kward/tools/registry.rb +62 -16
  101. data/lib/kward/tools/tool_call.rb +10 -0
  102. data/lib/kward/version.rb +1 -1
  103. data/lib/kward/workers/git_guard.rb +68 -0
  104. data/lib/kward/workers/live_view.rb +49 -0
  105. data/lib/kward/workers/manager.rb +288 -0
  106. data/lib/kward/workers/store.rb +72 -0
  107. data/lib/kward/workers/tool_policy.rb +23 -0
  108. data/lib/kward/workers/worker.rb +82 -0
  109. data/lib/kward/workers/write_lock.rb +38 -0
  110. data/lib/kward/workers.rb +7 -0
  111. data/lib/kward/workspace.rb +110 -24
  112. data/templates/default/fulldoc/html/css/kward.css +107 -36
  113. data/templates/default/kward_navigation.rb +12 -1
  114. data/templates/default/layout/html/layout.erb +4 -2
  115. data/templates/default/layout/html/setup.rb +6 -0
  116. metadata +53 -1
@@ -0,0 +1,23 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Interactive terminal UI used by the CLI frontend.
4
+ class PromptInterface
5
+ # User-facing default status text for editor buffers.
6
+ module EditorStatusText
7
+ module_function
8
+
9
+ def default(readonly:, editor_mode:)
10
+ return "Read-only diff · arrows/PageUp/PageDown move · Ctrl+F search · Ctrl+Q close" if readonly
11
+
12
+ case editor_mode
13
+ when "emacs"
14
+ "C-x C-s save · C-x C-c quit · C-s search"
15
+ when "vibe"
16
+ "NORMAL · i insert · :w save · :q quit"
17
+ else
18
+ "Ctrl+S save · Ctrl+Q quit · Ctrl+F search · Ctrl+C copy"
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,420 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Interactive terminal UI used by the CLI frontend.
4
+ class PromptInterface
5
+ # Lightweight line-oriented syntax highlighting for the built-in editor.
6
+ module EditorSyntaxHighlighter
7
+ RUBY_KEYWORDS = %w[
8
+ BEGIN END alias and begin break case class def defined? do else elsif end ensure
9
+ false for if in module next nil not or redo rescue retry return self super then true
10
+ undef unless until when while yield
11
+ ].freeze
12
+ RUBY_PATTERN = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|:[a-zA-Z_]\w*[!?=]?|\b\d+(?:\.\d+)?\b|\b[A-Z]\w*\b|\b(?:#{Regexp.union(RUBY_KEYWORDS)})\b)/.freeze
13
+ MARKDOWN_PATTERN = /(`[^`\n]+`|!?\[[^\]\n]+\]\([^\)\n]+\)|(?:\*\*|__)[^\n]+?(?:\*\*|__)|(?:\*|_)[^\n]+?(?:\*|_))/.freeze
14
+ HTML_PATTERN = /(<!--.*?-->|<\/?[A-Za-z][^>]*>|\b[A-Za-z_:-]+(?=\=)|"[^"]*"|'[^']*')/.freeze
15
+ CSS_PATTERN = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|#[0-9a-fA-F]{3,8}\b|\.[A-Za-z_-][\w-]*|#[A-Za-z_-][\w-]*|\b\d+(?:\.\d+)?(?:px|em|rem|%|vh|vw|s|ms)?\b|[A-Za-z_-][\w-]*(?=\s*:)|@[A-Za-z_-][\w-]*)/.freeze
16
+ JSON_PATTERN = /("(?:\\.|[^"\\])*"(?=\s*:)|"(?:\\.|[^"\\])*"|\b(?:true|false|null)\b|-?\b\d+(?:\.\d+)?(?:[eE][+-]?\d+)?\b)/.freeze
17
+ YAML_PATTERN = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\b(?:true|false|null|yes|no|on|off)\b|\b\d+(?:\.\d+)?\b|[A-Za-z0-9_-]+(?=\s*:))/.freeze
18
+ SQL_PATTERN = /("(?:\\.|[^"\\])*"|'(?:''|[^'])*'|--.*|\b\d+(?:\.\d+)?\b|\b(?:SELECT|FROM|WHERE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|INSERT|INTO|UPDATE|DELETE|CREATE|ALTER|DROP|TABLE|VIEW|INDEX|VALUES|SET|AND|OR|NOT|NULL|IS|AS|ORDER|BY|GROUP|HAVING|LIMIT|OFFSET|DISTINCT|UNION|ALL|CASE|WHEN|THEN|ELSE|END|PRIMARY|KEY|FOREIGN|REFERENCES|DEFAULT|TRUE|FALSE)\b)/i.freeze
19
+ GENERIC_STRING_NUMBER_PATTERN = /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:\.\d+)?\b)/.freeze
20
+
21
+ LANGUAGE_DEFINITIONS = {
22
+ javascript: {
23
+ extensions: %w[.js .jsx .mjs .cjs],
24
+ keywords: %w[async await break case catch class const continue debugger default delete do else export extends false finally for from function if import in instanceof let new null of return static super switch this throw true try typeof undefined var void while with yield]
25
+ },
26
+ typescript: {
27
+ extensions: %w[.ts .tsx],
28
+ keywords: %w[abstract any as async await boolean break case catch class const constructor continue debugger declare default delete do else enum export extends false finally for from function if implements import in infer instanceof interface is keyof let module namespace never new null number object of private protected public readonly return static string super switch symbol this throw true try type typeof undefined unknown var void while with yield]
29
+ },
30
+ shell: {
31
+ extensions: %w[.sh .bash .zsh .fish],
32
+ filenames: %w[.bashrc .bash_profile .zshrc .profile],
33
+ line_comment: "#",
34
+ keywords: %w[if then else elif fi for while until do done case esac function in select time coproc true false]
35
+ },
36
+ crystal: {
37
+ extensions: %w[.cr],
38
+ line_comment: "#",
39
+ keywords: %w[if unless while for do end enum struct macro union lib annotation def class module case begin until else elsif ensure rescue]
40
+ },
41
+ elixir: {
42
+ extensions: %w[.ex .exs],
43
+ line_comment: "#",
44
+ keywords: %w[def defp defmodule defprotocol defimpl defmacro do end fn case cond if unless try receive rescue after else true false nil]
45
+ },
46
+ julia: {
47
+ extensions: %w[.jl],
48
+ line_comment: "#",
49
+ keywords: %w[begin if while for try let quote function macro module baremodule struct mutable abstract primitive type do end else elseif catch finally true false nothing]
50
+ },
51
+ makefile: {
52
+ filenames: %w[Makefile makefile GNUmakefile],
53
+ line_comment: "#",
54
+ keywords: %w[ifeq ifneq ifdef ifndef else endif include define endef export unexport override]
55
+ },
56
+ python: {
57
+ extensions: %w[.py .pyw],
58
+ line_comment: "#",
59
+ keywords: %w[and as assert async await break class continue def del elif else except False finally for from global if import in is lambda None nonlocal not or pass raise return True try while with yield]
60
+ },
61
+ go: {
62
+ extensions: %w[.go],
63
+ keywords: %w[break case chan const continue default defer else fallthrough false for func go goto if import interface map nil package range return select struct switch true type var]
64
+ },
65
+ rust: {
66
+ extensions: %w[.rs],
67
+ keywords: %w[as async await break const continue crate dyn else enum extern false fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait true type unsafe use where while]
68
+ },
69
+ java: {
70
+ extensions: %w[.java],
71
+ keywords: %w[abstract assert boolean break byte case catch char class const continue default do double else enum extends false final finally float for if implements import instanceof int interface long native new null package private protected public return short static strictfp super switch synchronized this throw throws transient true try void volatile while]
72
+ },
73
+ csharp: {
74
+ extensions: %w[.cs],
75
+ keywords: %w[abstract as base bool break byte case catch char checked class const continue decimal default delegate do double else enum event explicit extern false finally fixed float for foreach goto if implicit in int interface internal is lock long namespace new null object operator out override params private protected public readonly ref return sbyte sealed short sizeof stackalloc static string struct switch this throw true try typeof uint ulong unchecked unsafe ushort using virtual void volatile while var async await]
76
+ },
77
+ c: {
78
+ extensions: %w[.c .h],
79
+ keywords: %w[auto break case char const continue default do double else enum extern float for goto if inline int long register restrict return short signed sizeof static struct switch typedef union unsigned void volatile while]
80
+ },
81
+ cpp: {
82
+ extensions: %w[.cc .cpp .cxx .hpp .hh .hxx],
83
+ keywords: %w[alignas alignof and asm auto bool break case catch char char16_t char32_t class const constexpr const_cast continue decltype default delete do double dynamic_cast else enum explicit export extern false float for friend goto if inline int long mutable namespace new noexcept nullptr operator or private protected public register reinterpret_cast return short signed sizeof static static_assert static_cast struct switch template this throw true try typedef typeid typename union unsigned using virtual void volatile wchar_t while]
84
+ },
85
+ swift: {
86
+ extensions: %w[.swift],
87
+ keywords: %w[as associatedtype break case catch class continue default defer deinit do else enum extension false fileprivate for func guard if import in init inout internal is let nil open operator private protocol public repeat rethrows return self Self static struct subscript super switch throw throws true try typealias var where while]
88
+ },
89
+ kotlin: {
90
+ extensions: %w[.kt .kts],
91
+ keywords: %w[as break class continue do else false for fun if in interface is null object package return super this throw true try typealias typeof val var when while by catch constructor delegate dynamic field file finally get import init param property receiver set setparam where actual abstract annotation companion const crossinline data enum expect external final infix inline inner internal lateinit noinline open operator out override private protected public reified sealed suspend tailrec vararg]
92
+ },
93
+ lua: {
94
+ extensions: %w[.lua],
95
+ line_comment: "--",
96
+ keywords: %w[and break do else elseif end false for function goto if in local nil not or repeat return then true until while]
97
+ }
98
+ }.freeze
99
+
100
+ RUBY_FILENAMES = %w[Gemfile Rakefile Guardfile Capfile Thorfile Vagrantfile].freeze
101
+ RUBY_EXTENSIONS = %w[.rb .rake .gemspec].freeze
102
+ MARKDOWN_EXTENSIONS = %w[.md .markdown].freeze
103
+ JSON_EXTENSIONS = %w[.json].freeze
104
+ YAML_EXTENSIONS = %w[.yml .yaml].freeze
105
+ HTML_EXTENSIONS = %w[.html .htm].freeze
106
+ CSS_EXTENSIONS = %w[.css].freeze
107
+ SCSS_EXTENSIONS = %w[.scss].freeze
108
+ SQL_EXTENSIONS = %w[.sql].freeze
109
+
110
+ private
111
+
112
+ def editor_highlight_line(line, line_index = nil)
113
+ return line.to_s unless @color_enabled
114
+
115
+ case editor_syntax_language
116
+ when :ruby
117
+ editor_highlight_ruby(line, line_index)
118
+ when :markdown
119
+ editor_highlight_markdown(line)
120
+ when :json
121
+ editor_highlight_json(line)
122
+ when :yaml
123
+ editor_highlight_yaml(line)
124
+ when :html
125
+ editor_highlight_html(line)
126
+ when :css, :scss
127
+ editor_highlight_css(line)
128
+ when :sql
129
+ editor_highlight_sql(line)
130
+ else
131
+ editor_highlight_generic(line, editor_syntax_language, line_index)
132
+ end
133
+ end
134
+
135
+ def editor_syntax_language
136
+ return nil unless @editor_state
137
+
138
+ @editor_syntax_language_path ||= nil
139
+ if @editor_syntax_language_path != @editor_state.path
140
+ @editor_syntax_language_path = @editor_state.path
141
+ @editor_syntax_language = editor_detect_syntax_language(@editor_state.path)
142
+ end
143
+ @editor_syntax_language
144
+ end
145
+
146
+ def editor_detect_syntax_language(path)
147
+ basename = File.basename(path.to_s)
148
+ extension = File.extname(basename).downcase
149
+ return :ruby if RUBY_FILENAMES.include?(basename) || RUBY_EXTENSIONS.include?(extension)
150
+ return :markdown if MARKDOWN_EXTENSIONS.include?(extension)
151
+ return :json if JSON_EXTENSIONS.include?(extension)
152
+ return :yaml if YAML_EXTENSIONS.include?(extension)
153
+ return :html if HTML_EXTENSIONS.include?(extension)
154
+ return :scss if SCSS_EXTENSIONS.include?(extension)
155
+ return :css if CSS_EXTENSIONS.include?(extension)
156
+ return :sql if SQL_EXTENSIONS.include?(extension)
157
+
158
+ LANGUAGE_DEFINITIONS.each do |language, definition|
159
+ return language if definition.fetch(:extensions, []).include?(extension)
160
+ return language if definition.fetch(:filenames, []).include?(basename)
161
+ end
162
+ nil
163
+ end
164
+
165
+ def editor_highlight_ruby(line, line_index = nil)
166
+ text = line.to_s
167
+ return colored(text, :gray) if editor_ruby_block_comment_line?(line_index)
168
+
169
+ comment_index = editor_comment_index(text, "#")
170
+ return editor_highlight_ruby_code(text) unless comment_index
171
+
172
+ editor_highlight_ruby_code(text[0...comment_index].to_s) + colored(text[comment_index..].to_s, :gray)
173
+ end
174
+
175
+ def editor_highlight_ruby_code(line)
176
+ line.to_s.gsub(RUBY_PATTERN) do |token|
177
+ editor_highlight_ruby_token(token)
178
+ end
179
+ end
180
+
181
+ def editor_ruby_block_comment_line?(line_index)
182
+ return false unless line_index && @editor_state
183
+
184
+ in_comment = false
185
+ @editor_state.lines.first(line_index.to_i + 1).each_with_index do |line, index|
186
+ starts_block = line.match?(/\A=begin\b/)
187
+ ends_block = line.match?(/\A=end\b/)
188
+ return true if index == line_index && (in_comment || starts_block || ends_block)
189
+
190
+ in_comment = true if starts_block
191
+ in_comment = false if ends_block
192
+ end
193
+ false
194
+ end
195
+
196
+ def editor_highlight_ruby_token(token)
197
+ if token.start_with?("\"", "'")
198
+ colored(token, :green)
199
+ elsif token.start_with?(":")
200
+ colored(token, :cyan)
201
+ elsif token.match?(/\A\d/)
202
+ colored(token, :magenta)
203
+ elsif RUBY_KEYWORDS.include?(token)
204
+ colored(token, :blue)
205
+ elsif token.match?(/\A[A-Z]/)
206
+ colored(token, :yellow)
207
+ else
208
+ token
209
+ end
210
+ end
211
+
212
+ def editor_highlight_generic(line, language, line_index = nil)
213
+ definition = LANGUAGE_DEFINITIONS[language]
214
+ return line.to_s unless definition
215
+
216
+ text = line.to_s
217
+ return colored(text, :gray) if editor_c_style_block_comment_line?(line_index)
218
+
219
+ marker = definition[:line_comment] || "//"
220
+ comment_index = editor_comment_index(text, marker)
221
+ return editor_highlight_generic_code(text, definition[:keywords]) unless comment_index
222
+
223
+ editor_highlight_generic_code(text[0...comment_index].to_s, definition[:keywords]) + colored(text[comment_index..].to_s, :gray)
224
+ end
225
+
226
+ def editor_highlight_generic_code(line, keywords)
227
+ pattern = editor_generic_pattern(keywords)
228
+ line.to_s.gsub(pattern) do |token|
229
+ editor_highlight_generic_token(token, keywords)
230
+ end
231
+ end
232
+
233
+ def editor_generic_pattern(keywords)
234
+ /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:\.\d+)?\b|\b[A-Z]\w*\b|\b(?:#{Regexp.union(keywords)})\b)/
235
+ end
236
+
237
+ def editor_highlight_generic_token(token, keywords)
238
+ if token.start_with?("\"", "'", "`")
239
+ colored(token, :green)
240
+ elsif token.match?(/\A\d/)
241
+ colored(token, :magenta)
242
+ elsif keywords.include?(token)
243
+ colored(token, :blue)
244
+ elsif token.match?(/\A[A-Z]/)
245
+ colored(token, :yellow)
246
+ else
247
+ token
248
+ end
249
+ end
250
+
251
+ def editor_c_style_block_comment_line?(line_index)
252
+ return false unless line_index && @editor_state
253
+
254
+ in_comment = false
255
+ @editor_state.lines.first(line_index.to_i + 1).each_with_index do |line, index|
256
+ starts_block = editor_comment_index(line, "/*")
257
+ ends_block = in_comment && line.include?("*/")
258
+ return true if index == line_index && (in_comment || starts_block)
259
+
260
+ in_comment = true if starts_block && !line[starts_block..].to_s.include?("*/")
261
+ in_comment = false if ends_block
262
+ end
263
+ false
264
+ end
265
+
266
+ def editor_comment_index(line, marker)
267
+ quote = nil
268
+ escaped = false
269
+ text = line.to_s
270
+ index = 0
271
+ while index < text.length
272
+ char = text[index]
273
+ if quote
274
+ if escaped
275
+ escaped = false
276
+ elsif char == "\\"
277
+ escaped = true
278
+ elsif char == quote
279
+ quote = nil
280
+ end
281
+ elsif char == "\"" || char == "'" || char == "`"
282
+ quote = char
283
+ elsif text[index, marker.length] == marker
284
+ return index
285
+ end
286
+ index += 1
287
+ end
288
+ nil
289
+ end
290
+
291
+ def editor_highlight_markdown(line)
292
+ text = line.to_s
293
+ return editor_highlight_markdown_heading(text) if text.match?(/\A\s{0,3}[#]{1,6}\s/)
294
+ return editor_highlight_markdown_fence(text) if text.match?(/\A\s*```/)
295
+ return editor_highlight_markdown_blockquote(text) if text.match?(/\A\s*>/)
296
+ return editor_highlight_markdown_list(text) if text.match?(/\A\s*(?:[-*+]\s+|\d+\.\s+)/)
297
+
298
+ editor_highlight_markdown_inline(text)
299
+ end
300
+
301
+ def editor_highlight_markdown_heading(line)
302
+ line.sub(/\A(\s{0,3}[#]{1,6}\s+)(.*)\z/) do
303
+ "#{colored(Regexp.last_match(1), :cyan)}#{colored(Regexp.last_match(2), :bold)}"
304
+ end
305
+ end
306
+
307
+ def editor_highlight_markdown_fence(line)
308
+ colored(line, :gray)
309
+ end
310
+
311
+ def editor_highlight_markdown_blockquote(line)
312
+ line.sub(/\A(\s*>\s?)(.*)\z/) do
313
+ "#{colored(Regexp.last_match(1), :gray)}#{editor_highlight_markdown_inline(Regexp.last_match(2))}"
314
+ end
315
+ end
316
+
317
+ def editor_highlight_markdown_list(line)
318
+ line.sub(/\A(\s*(?:[-*+]\s+|\d+\.\s+))(.*)\z/) do
319
+ "#{colored(Regexp.last_match(1), :cyan)}#{editor_highlight_markdown_inline(Regexp.last_match(2))}"
320
+ end
321
+ end
322
+
323
+ def editor_highlight_markdown_inline(line)
324
+ line.to_s.gsub(MARKDOWN_PATTERN) do |token|
325
+ if token.start_with?("`")
326
+ colored(token, :dim)
327
+ elsif token.start_with?("[", "![")
328
+ colored(token, :blue)
329
+ else
330
+ colored(token, :bold)
331
+ end
332
+ end
333
+ end
334
+
335
+ def editor_highlight_json(line)
336
+ line.to_s.gsub(JSON_PATTERN) do |token|
337
+ if token.start_with?("\"") && token.end_with?("\"")
338
+ Regexp.last_match.post_match.match?(/\A\s*:/) ? colored(token, :cyan) : colored(token, :green)
339
+ elsif token.match?(/\A-?\d/)
340
+ colored(token, :magenta)
341
+ else
342
+ colored(token, :blue)
343
+ end
344
+ end
345
+ end
346
+
347
+ def editor_highlight_yaml(line)
348
+ text = line.to_s
349
+ comment_index = editor_comment_index(text, "#")
350
+ highlighted = if comment_index
351
+ editor_highlight_yaml_code(text[0...comment_index].to_s) + colored(text[comment_index..].to_s, :gray)
352
+ else
353
+ editor_highlight_yaml_code(text)
354
+ end
355
+ highlighted
356
+ end
357
+
358
+ def editor_highlight_yaml_code(line)
359
+ line.to_s.gsub(YAML_PATTERN) do |token|
360
+ if Regexp.last_match.post_match.match?(/\A\s*:/)
361
+ colored(token, :cyan)
362
+ elsif token.start_with?("\"", "'")
363
+ colored(token, :green)
364
+ elsif token.match?(/\A\d/)
365
+ colored(token, :magenta)
366
+ else
367
+ colored(token, :blue)
368
+ end
369
+ end
370
+ end
371
+
372
+ def editor_highlight_html(line)
373
+ line.to_s.gsub(HTML_PATTERN) do |token|
374
+ if token.start_with?("<!--")
375
+ colored(token, :gray)
376
+ elsif token.start_with?("<")
377
+ colored(token, :blue)
378
+ elsif token.start_with?("\"", "'")
379
+ colored(token, :green)
380
+ else
381
+ colored(token, :cyan)
382
+ end
383
+ end
384
+ end
385
+
386
+ def editor_highlight_css(line)
387
+ text = line.to_s
388
+ return colored(text, :gray) if text.strip.start_with?("/*")
389
+
390
+ text.gsub(CSS_PATTERN) do |token|
391
+ if token.start_with?("\"", "'")
392
+ colored(token, :green)
393
+ elsif token.start_with?("#") && token.match?(/\A#[0-9a-fA-F]/)
394
+ colored(token, :magenta)
395
+ elsif token.start_with?(".", "#", "@")
396
+ colored(token, :cyan)
397
+ elsif token.match?(/\A\d/)
398
+ colored(token, :magenta)
399
+ else
400
+ colored(token, :blue)
401
+ end
402
+ end
403
+ end
404
+
405
+ def editor_highlight_sql(line)
406
+ line.to_s.gsub(SQL_PATTERN) do |token|
407
+ if token.start_with?("--")
408
+ colored(token, :gray)
409
+ elsif token.start_with?("\"", "'")
410
+ colored(token, :green)
411
+ elsif token.match?(/\A\d/)
412
+ colored(token, :magenta)
413
+ else
414
+ colored(token, :blue)
415
+ end
416
+ end
417
+ end
418
+ end
419
+ end
420
+ end
@@ -0,0 +1,46 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Interactive terminal UI used by the CLI frontend.
4
+ class PromptInterface
5
+ # Bounded undo/redo history for editor buffer snapshots.
6
+ class EditorUndoHistory
7
+ attr_reader :undo_stack, :redo_stack
8
+
9
+ def initialize(limit: 100, undo_stack: [], redo_stack: [])
10
+ @limit = limit
11
+ @undo_stack = undo_stack
12
+ @redo_stack = redo_stack
13
+ end
14
+
15
+ def push(snapshot)
16
+ @undo_stack << snapshot
17
+ trim(@undo_stack)
18
+ @redo_stack.clear
19
+ end
20
+
21
+ def undo(current_snapshot)
22
+ snapshot = @undo_stack.pop
23
+ return nil unless snapshot
24
+
25
+ @redo_stack << current_snapshot
26
+ trim(@redo_stack)
27
+ snapshot
28
+ end
29
+
30
+ def redo(current_snapshot)
31
+ snapshot = @redo_stack.pop
32
+ return nil unless snapshot
33
+
34
+ @undo_stack << current_snapshot
35
+ trim(@undo_stack)
36
+ snapshot
37
+ end
38
+
39
+ private
40
+
41
+ def trim(stack)
42
+ stack.shift while stack.length > @limit
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,44 @@
1
+ # Namespace for the Kward CLI agent runtime.
2
+ module Kward
3
+ # Interactive terminal UI used by the CLI frontend.
4
+ class PromptInterface
5
+ # Modal Vibe editor state for one editor buffer.
6
+ class VibeEditorState
7
+ attr_accessor :mode, :pending, :command, :last_change, :last_find
8
+ attr_accessor :last_visual_selection, :visual_block_insert
9
+ attr_accessor :marks, :registers, :macros, :recording_macro, :last_macro
10
+
11
+ def initialize(editor_mode: "modern")
12
+ @mode = editor_mode == "vibe" ? "normal" : nil
13
+ @pending = ""
14
+ @command = ""
15
+ @last_change = nil
16
+ @last_find = nil
17
+ @last_visual_selection = nil
18
+ @visual_block_insert = nil
19
+ @marks = {}
20
+ @registers = {}
21
+ @macros = {}
22
+ @recording_macro = nil
23
+ @last_macro = nil
24
+ end
25
+
26
+ def self.copy(other)
27
+ state = new(editor_mode: other.mode ? "vibe" : "modern")
28
+ state.mode = other.mode&.dup
29
+ state.pending = other.pending.dup
30
+ state.command = other.command.dup
31
+ state.last_change = other.last_change&.dup
32
+ state.last_find = other.last_find&.dup
33
+ state.last_visual_selection = other.last_visual_selection&.dup
34
+ state.visual_block_insert = other.visual_block_insert&.dup
35
+ state.marks = other.marks.transform_values(&:dup)
36
+ state.registers = other.registers.transform_values(&:dup)
37
+ state.macros = other.macros.transform_values(&:dup)
38
+ state.recording_macro = other.recording_macro
39
+ state.last_macro = other.last_macro
40
+ state
41
+ end
42
+ end
43
+ end
44
+ end