kward 0.71.0 → 0.73.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/ci.yml +30 -0
  3. data/CHANGELOG.md +93 -0
  4. data/Gemfile.lock +2 -2
  5. data/README.md +4 -0
  6. data/doc/agent-tools.md +15 -6
  7. data/doc/authentication.md +22 -1
  8. data/doc/code-search.md +42 -2
  9. data/doc/configuration.md +106 -3
  10. data/doc/context-budgeting.md +136 -0
  11. data/doc/context-tools.md +16 -3
  12. data/doc/editor.md +415 -0
  13. data/doc/extensibility.md +16 -7
  14. data/doc/files.md +100 -0
  15. data/doc/getting-started.md +25 -18
  16. data/doc/git.md +123 -0
  17. data/doc/memory.md +24 -4
  18. data/doc/personas.md +34 -5
  19. data/doc/plugins.md +72 -1
  20. data/doc/releasing.md +37 -9
  21. data/doc/rpc.md +75 -5
  22. data/doc/session-management.md +35 -1
  23. data/doc/shell.md +332 -0
  24. data/doc/tabs.md +122 -0
  25. data/doc/troubleshooting.md +77 -1
  26. data/doc/usage.md +79 -7
  27. data/doc/web-search.md +12 -4
  28. data/doc/workspace-tools.md +51 -12
  29. data/examples/plugins/space_invaders.rb +377 -0
  30. data/lib/kward/agent.rb +1 -1
  31. data/lib/kward/ansi.rb +62 -23
  32. data/lib/kward/cli/commands.rb +33 -2
  33. data/lib/kward/cli/git.rb +150 -0
  34. data/lib/kward/cli/interactive_turn.rb +73 -9
  35. data/lib/kward/cli/plugins.rb +54 -4
  36. data/lib/kward/cli/prompt_interface.rb +32 -1
  37. data/lib/kward/cli/rendering.rb +4 -1
  38. data/lib/kward/cli/runtime_helpers.rb +268 -4
  39. data/lib/kward/cli/sessions.rb +2 -2
  40. data/lib/kward/cli/settings.rb +217 -9
  41. data/lib/kward/cli/slash_commands.rb +628 -2
  42. data/lib/kward/cli/tabs.rb +725 -0
  43. data/lib/kward/cli/tool_summaries.rb +6 -0
  44. data/lib/kward/cli.rb +150 -26
  45. data/lib/kward/clipboard.rb +2 -3
  46. data/lib/kward/compactor.rb +7 -19
  47. data/lib/kward/config_files.rb +145 -1
  48. data/lib/kward/context_budget_meter.rb +44 -0
  49. data/lib/kward/conversation.rb +12 -4
  50. data/lib/kward/editor_mode.rb +25 -0
  51. data/lib/kward/ekwsh.rb +559 -0
  52. data/lib/kward/image_attachments.rb +3 -1
  53. data/lib/kward/interactive_pty_runner.rb +151 -0
  54. data/lib/kward/local_command_runner.rb +155 -0
  55. data/lib/kward/local_pty_command_runner.rb +171 -0
  56. data/lib/kward/model/context_usage.rb +2 -2
  57. data/lib/kward/model/payloads.rb +2 -5
  58. data/lib/kward/plugin_registry.rb +61 -0
  59. data/lib/kward/project_files.rb +52 -0
  60. data/lib/kward/prompt_history.rb +84 -0
  61. data/lib/kward/prompt_interface/composer_controller.rb +69 -1
  62. data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
  63. data/lib/kward/prompt_interface/composer_state.rb +96 -27
  64. data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
  65. data/lib/kward/prompt_interface/editor/auto_indent.rb +510 -0
  66. data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
  67. data/lib/kward/prompt_interface/editor/controller.rb +1218 -0
  68. data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
  69. data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
  70. data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
  71. data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
  72. data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
  73. data/lib/kward/prompt_interface/editor/modes/modern.rb +354 -0
  74. data/lib/kward/prompt_interface/editor/modes/vibe.rb +1812 -0
  75. data/lib/kward/prompt_interface/editor/modes/vibe_insert_readline.rb +166 -0
  76. data/lib/kward/prompt_interface/editor/renderer.rb +244 -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 +1271 -0
  80. data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
  81. data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +422 -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 +288 -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 +451 -57
  90. data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
  91. data/lib/kward/prompt_interface/project_browser.rb +524 -0
  92. data/lib/kward/prompt_interface/question_prompt.rb +99 -56
  93. data/lib/kward/prompt_interface/runtime_state.rb +43 -0
  94. data/lib/kward/prompt_interface/screen.rb +19 -3
  95. data/lib/kward/prompt_interface/selection_prompt.rb +10 -19
  96. data/lib/kward/prompt_interface/slash_overlay.rb +2 -0
  97. data/lib/kward/prompt_interface/stream_state.rb +7 -0
  98. data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
  99. data/lib/kward/prompt_interface.rb +366 -222
  100. data/lib/kward/prompts/commands.rb +9 -0
  101. data/lib/kward/prompts.rb +2 -0
  102. data/lib/kward/rpc/memory_methods.rb +83 -0
  103. data/lib/kward/rpc/server.rb +169 -83
  104. data/lib/kward/rpc/session_manager.rb +45 -121
  105. data/lib/kward/rpc/session_tree_rows.rb +9 -115
  106. data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
  107. data/lib/kward/rpc/tool_metadata.rb +11 -0
  108. data/lib/kward/rpc/transcript_normalizer.rb +4 -39
  109. data/lib/kward/scratchpad_runner.rb +56 -0
  110. data/lib/kward/session_diff.rb +20 -3
  111. data/lib/kward/session_naming.rb +11 -0
  112. data/lib/kward/session_store.rb +44 -0
  113. data/lib/kward/session_tree_nodes.rb +136 -0
  114. data/lib/kward/session_tree_renderer.rb +9 -131
  115. data/lib/kward/tab_store.rb +47 -0
  116. data/lib/kward/terminal_keys.rb +84 -0
  117. data/lib/kward/terminal_sequences.rb +42 -0
  118. data/lib/kward/text_boundary.rb +25 -0
  119. data/lib/kward/tools/context_budget_stats.rb +54 -0
  120. data/lib/kward/tools/context_for_task.rb +204 -0
  121. data/lib/kward/tools/read_file.rb +8 -4
  122. data/lib/kward/tools/registry.rb +62 -16
  123. data/lib/kward/tools/tool_call.rb +10 -0
  124. data/lib/kward/version.rb +1 -1
  125. data/lib/kward/workers/git_guard.rb +93 -0
  126. data/lib/kward/workers/job.rb +99 -0
  127. data/lib/kward/workers/live_view.rb +49 -0
  128. data/lib/kward/workers/manager.rb +288 -0
  129. data/lib/kward/workers/queue_runner.rb +166 -0
  130. data/lib/kward/workers/queue_store.rb +112 -0
  131. data/lib/kward/workers/store.rb +72 -0
  132. data/lib/kward/workers/tool_policy.rb +23 -0
  133. data/lib/kward/workers/worker.rb +82 -0
  134. data/lib/kward/workers/write_lock.rb +38 -0
  135. data/lib/kward/workers.rb +10 -0
  136. data/lib/kward/workspace.rb +125 -87
  137. data/templates/default/fulldoc/html/css/kward.css +140 -36
  138. data/templates/default/fulldoc/html/images/kward_screen_1.png +0 -0
  139. data/templates/default/fulldoc/html/setup.rb +1 -0
  140. data/templates/default/kward_navigation.rb +12 -1
  141. data/templates/default/layout/html/layout.erb +23 -34
  142. data/templates/default/layout/html/setup.rb +6 -0
  143. metadata +67 -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,422 @@
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
+ return @editor_state.language if @editor_state.language
138
+
139
+ path = @editor_state.path || @editor_state.display_path
140
+ @editor_syntax_language_path ||= nil
141
+ if @editor_syntax_language_path != path
142
+ @editor_syntax_language_path = path
143
+ @editor_syntax_language = editor_detect_syntax_language(path)
144
+ end
145
+ @editor_syntax_language
146
+ end
147
+
148
+ def editor_detect_syntax_language(path)
149
+ basename = File.basename(path.to_s)
150
+ extension = File.extname(basename).downcase
151
+ return :ruby if RUBY_FILENAMES.include?(basename) || RUBY_EXTENSIONS.include?(extension)
152
+ return :markdown if MARKDOWN_EXTENSIONS.include?(extension)
153
+ return :json if JSON_EXTENSIONS.include?(extension)
154
+ return :yaml if YAML_EXTENSIONS.include?(extension)
155
+ return :html if HTML_EXTENSIONS.include?(extension)
156
+ return :scss if SCSS_EXTENSIONS.include?(extension)
157
+ return :css if CSS_EXTENSIONS.include?(extension)
158
+ return :sql if SQL_EXTENSIONS.include?(extension)
159
+
160
+ LANGUAGE_DEFINITIONS.each do |language, definition|
161
+ return language if definition.fetch(:extensions, []).include?(extension)
162
+ return language if definition.fetch(:filenames, []).include?(basename)
163
+ end
164
+ nil
165
+ end
166
+
167
+ def editor_highlight_ruby(line, line_index = nil)
168
+ text = line.to_s
169
+ return colored(text, :gray) if editor_ruby_block_comment_line?(line_index)
170
+
171
+ comment_index = editor_comment_index(text, "#")
172
+ return editor_highlight_ruby_code(text) unless comment_index
173
+
174
+ editor_highlight_ruby_code(text[0...comment_index].to_s) + colored(text[comment_index..].to_s, :gray)
175
+ end
176
+
177
+ def editor_highlight_ruby_code(line)
178
+ line.to_s.gsub(RUBY_PATTERN) do |token|
179
+ editor_highlight_ruby_token(token)
180
+ end
181
+ end
182
+
183
+ def editor_ruby_block_comment_line?(line_index)
184
+ return false unless line_index && @editor_state
185
+
186
+ in_comment = false
187
+ @editor_state.lines.first(line_index.to_i + 1).each_with_index do |line, index|
188
+ starts_block = line.match?(/\A=begin\b/)
189
+ ends_block = line.match?(/\A=end\b/)
190
+ return true if index == line_index && (in_comment || starts_block || ends_block)
191
+
192
+ in_comment = true if starts_block
193
+ in_comment = false if ends_block
194
+ end
195
+ false
196
+ end
197
+
198
+ def editor_highlight_ruby_token(token)
199
+ if token.start_with?("\"", "'")
200
+ colored(token, :green)
201
+ elsif token.start_with?(":")
202
+ colored(token, :cyan)
203
+ elsif token.match?(/\A\d/)
204
+ colored(token, :magenta)
205
+ elsif RUBY_KEYWORDS.include?(token)
206
+ colored(token, :blue)
207
+ elsif token.match?(/\A[A-Z]/)
208
+ colored(token, :yellow)
209
+ else
210
+ token
211
+ end
212
+ end
213
+
214
+ def editor_highlight_generic(line, language, line_index = nil)
215
+ definition = LANGUAGE_DEFINITIONS[language]
216
+ return line.to_s unless definition
217
+
218
+ text = line.to_s
219
+ return colored(text, :gray) if editor_c_style_block_comment_line?(line_index)
220
+
221
+ marker = definition[:line_comment] || "//"
222
+ comment_index = editor_comment_index(text, marker)
223
+ return editor_highlight_generic_code(text, definition[:keywords]) unless comment_index
224
+
225
+ editor_highlight_generic_code(text[0...comment_index].to_s, definition[:keywords]) + colored(text[comment_index..].to_s, :gray)
226
+ end
227
+
228
+ def editor_highlight_generic_code(line, keywords)
229
+ pattern = editor_generic_pattern(keywords)
230
+ line.to_s.gsub(pattern) do |token|
231
+ editor_highlight_generic_token(token, keywords)
232
+ end
233
+ end
234
+
235
+ def editor_generic_pattern(keywords)
236
+ /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:\.\d+)?\b|\b[A-Z]\w*\b|\b(?:#{Regexp.union(keywords)})\b)/
237
+ end
238
+
239
+ def editor_highlight_generic_token(token, keywords)
240
+ if token.start_with?("\"", "'", "`")
241
+ colored(token, :green)
242
+ elsif token.match?(/\A\d/)
243
+ colored(token, :magenta)
244
+ elsif keywords.include?(token)
245
+ colored(token, :blue)
246
+ elsif token.match?(/\A[A-Z]/)
247
+ colored(token, :yellow)
248
+ else
249
+ token
250
+ end
251
+ end
252
+
253
+ def editor_c_style_block_comment_line?(line_index)
254
+ return false unless line_index && @editor_state
255
+
256
+ in_comment = false
257
+ @editor_state.lines.first(line_index.to_i + 1).each_with_index do |line, index|
258
+ starts_block = editor_comment_index(line, "/*")
259
+ ends_block = in_comment && line.include?("*/")
260
+ return true if index == line_index && (in_comment || starts_block)
261
+
262
+ in_comment = true if starts_block && !line[starts_block..].to_s.include?("*/")
263
+ in_comment = false if ends_block
264
+ end
265
+ false
266
+ end
267
+
268
+ def editor_comment_index(line, marker)
269
+ quote = nil
270
+ escaped = false
271
+ text = line.to_s
272
+ index = 0
273
+ while index < text.length
274
+ char = text[index]
275
+ if quote
276
+ if escaped
277
+ escaped = false
278
+ elsif char == "\\"
279
+ escaped = true
280
+ elsif char == quote
281
+ quote = nil
282
+ end
283
+ elsif char == "\"" || char == "'" || char == "`"
284
+ quote = char
285
+ elsif text[index, marker.length] == marker
286
+ return index
287
+ end
288
+ index += 1
289
+ end
290
+ nil
291
+ end
292
+
293
+ def editor_highlight_markdown(line)
294
+ text = line.to_s
295
+ return editor_highlight_markdown_heading(text) if text.match?(/\A\s{0,3}[#]{1,6}\s/)
296
+ return editor_highlight_markdown_fence(text) if text.match?(/\A\s*```/)
297
+ return editor_highlight_markdown_blockquote(text) if text.match?(/\A\s*>/)
298
+ return editor_highlight_markdown_list(text) if text.match?(/\A\s*(?:[-*+]\s+|\d+\.\s+)/)
299
+
300
+ editor_highlight_markdown_inline(text)
301
+ end
302
+
303
+ def editor_highlight_markdown_heading(line)
304
+ line.sub(/\A(\s{0,3}[#]{1,6}\s+)(.*)\z/) do
305
+ "#{colored(Regexp.last_match(1), :cyan)}#{colored(Regexp.last_match(2), :bold)}"
306
+ end
307
+ end
308
+
309
+ def editor_highlight_markdown_fence(line)
310
+ colored(line, :gray)
311
+ end
312
+
313
+ def editor_highlight_markdown_blockquote(line)
314
+ line.sub(/\A(\s*>\s?)(.*)\z/) do
315
+ "#{colored(Regexp.last_match(1), :gray)}#{editor_highlight_markdown_inline(Regexp.last_match(2))}"
316
+ end
317
+ end
318
+
319
+ def editor_highlight_markdown_list(line)
320
+ line.sub(/\A(\s*(?:[-*+]\s+|\d+\.\s+))(.*)\z/) do
321
+ "#{colored(Regexp.last_match(1), :cyan)}#{editor_highlight_markdown_inline(Regexp.last_match(2))}"
322
+ end
323
+ end
324
+
325
+ def editor_highlight_markdown_inline(line)
326
+ line.to_s.gsub(MARKDOWN_PATTERN) do |token|
327
+ if token.start_with?("`")
328
+ colored(token, :dim)
329
+ elsif token.start_with?("[", "![")
330
+ colored(token, :blue)
331
+ else
332
+ colored(token, :bold)
333
+ end
334
+ end
335
+ end
336
+
337
+ def editor_highlight_json(line)
338
+ line.to_s.gsub(JSON_PATTERN) do |token|
339
+ if token.start_with?("\"") && token.end_with?("\"")
340
+ Regexp.last_match.post_match.match?(/\A\s*:/) ? colored(token, :cyan) : colored(token, :green)
341
+ elsif token.match?(/\A-?\d/)
342
+ colored(token, :magenta)
343
+ else
344
+ colored(token, :blue)
345
+ end
346
+ end
347
+ end
348
+
349
+ def editor_highlight_yaml(line)
350
+ text = line.to_s
351
+ comment_index = editor_comment_index(text, "#")
352
+ highlighted = if comment_index
353
+ editor_highlight_yaml_code(text[0...comment_index].to_s) + colored(text[comment_index..].to_s, :gray)
354
+ else
355
+ editor_highlight_yaml_code(text)
356
+ end
357
+ highlighted
358
+ end
359
+
360
+ def editor_highlight_yaml_code(line)
361
+ line.to_s.gsub(YAML_PATTERN) do |token|
362
+ if Regexp.last_match.post_match.match?(/\A\s*:/)
363
+ colored(token, :cyan)
364
+ elsif token.start_with?("\"", "'")
365
+ colored(token, :green)
366
+ elsif token.match?(/\A\d/)
367
+ colored(token, :magenta)
368
+ else
369
+ colored(token, :blue)
370
+ end
371
+ end
372
+ end
373
+
374
+ def editor_highlight_html(line)
375
+ line.to_s.gsub(HTML_PATTERN) do |token|
376
+ if token.start_with?("<!--")
377
+ colored(token, :gray)
378
+ elsif token.start_with?("<")
379
+ colored(token, :blue)
380
+ elsif token.start_with?("\"", "'")
381
+ colored(token, :green)
382
+ else
383
+ colored(token, :cyan)
384
+ end
385
+ end
386
+ end
387
+
388
+ def editor_highlight_css(line)
389
+ text = line.to_s
390
+ return colored(text, :gray) if text.strip.start_with?("/*")
391
+
392
+ text.gsub(CSS_PATTERN) do |token|
393
+ if token.start_with?("\"", "'")
394
+ colored(token, :green)
395
+ elsif token.start_with?("#") && token.match?(/\A#[0-9a-fA-F]/)
396
+ colored(token, :magenta)
397
+ elsif token.start_with?(".", "#", "@")
398
+ colored(token, :cyan)
399
+ elsif token.match?(/\A\d/)
400
+ colored(token, :magenta)
401
+ else
402
+ colored(token, :blue)
403
+ end
404
+ end
405
+ end
406
+
407
+ def editor_highlight_sql(line)
408
+ line.to_s.gsub(SQL_PATTERN) do |token|
409
+ if token.start_with?("--")
410
+ colored(token, :gray)
411
+ elsif token.start_with?("\"", "'")
412
+ colored(token, :green)
413
+ elsif token.match?(/\A\d/)
414
+ colored(token, :magenta)
415
+ else
416
+ colored(token, :blue)
417
+ end
418
+ end
419
+ end
420
+ end
421
+ end
422
+ 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