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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -1
- data/Gemfile.lock +2 -2
- data/README.md +4 -0
- data/doc/agent-tools.md +15 -6
- data/doc/authentication.md +22 -1
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +106 -3
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +16 -3
- data/doc/editor.md +394 -0
- data/doc/extensibility.md +16 -7
- data/doc/files.md +100 -0
- data/doc/getting-started.md +25 -18
- data/doc/git.md +122 -0
- data/doc/memory.md +24 -4
- data/doc/personas.md +34 -5
- data/doc/plugins.md +72 -1
- data/doc/releasing.md +37 -9
- data/doc/rpc.md +74 -4
- data/doc/session-management.md +35 -1
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +53 -7
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +51 -12
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +33 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +32 -1
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +2 -2
- data/lib/kward/cli/settings.rb +218 -9
- data/lib/kward/cli/slash_commands.rb +415 -2
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +158 -26
- data/lib/kward/config_files.rb +123 -1
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +12 -4
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/plugin_registry.rb +61 -0
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/composer_controller.rb +69 -1
- data/lib/kward/prompt_interface/composer_renderer.rb +109 -13
- data/lib/kward/prompt_interface/composer_state.rb +96 -27
- data/lib/kward/prompt_interface/editor/auto_close_pairs.rb +123 -0
- data/lib/kward/prompt_interface/editor/auto_indent.rb +509 -0
- data/lib/kward/prompt_interface/editor/buffer.rb +109 -0
- data/lib/kward/prompt_interface/editor/controller.rb +1018 -0
- data/lib/kward/prompt_interface/editor/endwise.rb +321 -0
- data/lib/kward/prompt_interface/editor/file_marker.rb +40 -0
- data/lib/kward/prompt_interface/editor/indent_navigation.rb +61 -0
- data/lib/kward/prompt_interface/editor/kill_ring.rb +78 -0
- data/lib/kward/prompt_interface/editor/modes/emacs.rb +259 -0
- data/lib/kward/prompt_interface/editor/modes/modern.rb +353 -0
- data/lib/kward/prompt_interface/editor/modes/vibe.rb +1962 -0
- data/lib/kward/prompt_interface/editor/renderer.rb +243 -0
- data/lib/kward/prompt_interface/editor/search.rb +76 -0
- data/lib/kward/prompt_interface/editor/selections.rb +120 -0
- data/lib/kward/prompt_interface/editor/state.rb +1249 -0
- data/lib/kward/prompt_interface/editor/status_text.rb +23 -0
- data/lib/kward/prompt_interface/editor/syntax_highlighter.rb +420 -0
- data/lib/kward/prompt_interface/editor/undo_history.rb +46 -0
- data/lib/kward/prompt_interface/editor/vibe_state.rb +44 -0
- data/lib/kward/prompt_interface/file_overlay.rb +211 -0
- data/lib/kward/prompt_interface/git_prompt.rb +299 -0
- data/lib/kward/prompt_interface/interactive/controller.rb +186 -0
- data/lib/kward/prompt_interface/interactive/renderer.rb +71 -0
- data/lib/kward/prompt_interface/interactive/state.rb +62 -0
- data/lib/kward/prompt_interface/key_handler.rb +387 -35
- data/lib/kward/prompt_interface/overlay_renderer.rb +21 -2
- data/lib/kward/prompt_interface/project_browser.rb +524 -0
- data/lib/kward/prompt_interface/question_prompt.rb +98 -50
- data/lib/kward/prompt_interface/runtime_state.rb +43 -0
- data/lib/kward/prompt_interface/screen.rb +16 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +7 -13
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +6 -0
- data/lib/kward/prompt_interface.rb +286 -8
- data/lib/kward/prompts/commands.rb +5 -0
- data/lib/kward/prompts.rb +2 -0
- data/lib/kward/rpc/server.rb +42 -3
- data/lib/kward/rpc/session_manager.rb +35 -47
- data/lib/kward/rpc/session_tree_rows.rb +9 -115
- data/lib/kward/rpc/tool_event_normalizer.rb +1 -1
- data/lib/kward/session_store.rb +44 -0
- data/lib/kward/session_tree_nodes.rb +136 -0
- data/lib/kward/session_tree_renderer.rb +9 -131
- data/lib/kward/tab_store.rb +47 -0
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tools/context_budget_stats.rb +54 -0
- data/lib/kward/tools/context_for_task.rb +202 -0
- data/lib/kward/tools/read_file.rb +8 -4
- data/lib/kward/tools/registry.rb +62 -16
- data/lib/kward/tools/tool_call.rb +10 -0
- data/lib/kward/version.rb +1 -1
- data/lib/kward/workers/git_guard.rb +68 -0
- data/lib/kward/workers/live_view.rb +49 -0
- data/lib/kward/workers/manager.rb +288 -0
- data/lib/kward/workers/store.rb +72 -0
- data/lib/kward/workers/tool_policy.rb +23 -0
- data/lib/kward/workers/worker.rb +82 -0
- data/lib/kward/workers/write_lock.rb +38 -0
- data/lib/kward/workers.rb +7 -0
- data/lib/kward/workspace.rb +110 -24
- data/templates/default/fulldoc/html/css/kward.css +107 -36
- data/templates/default/kward_navigation.rb +12 -1
- data/templates/default/layout/html/layout.erb +4 -2
- data/templates/default/layout/html/setup.rb +6 -0
- 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
|