kward 0.70.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/.github/workflows/pages.yml +1 -1
- data/CHANGELOG.md +89 -3
- data/Gemfile +2 -0
- data/Gemfile.lock +90 -2
- data/README.md +34 -6
- data/Rakefile +96 -0
- data/doc/agent-tools.md +52 -0
- data/doc/api.md +92 -0
- data/doc/authentication.md +58 -23
- data/doc/code-search.md +42 -2
- data/doc/configuration.md +102 -13
- data/doc/context-budgeting.md +136 -0
- data/doc/context-tools.md +83 -0
- 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 +74 -3
- data/doc/releasing.md +45 -8
- data/doc/rpc.md +77 -15
- data/doc/session-management.md +254 -0
- data/doc/shell.md +286 -0
- data/doc/tabs.md +122 -0
- data/doc/troubleshooting.md +77 -1
- data/doc/usage.md +60 -15
- data/doc/web-search.md +12 -4
- data/doc/workspace-tools.md +144 -0
- data/examples/plugins/space_invaders.rb +377 -0
- data/lib/kward/agent.rb +1 -1
- data/lib/kward/cli/commands.rb +41 -2
- data/lib/kward/cli/git.rb +150 -0
- data/lib/kward/cli/interactive_turn.rb +73 -9
- data/lib/kward/cli/openrouter_commands.rb +55 -0
- data/lib/kward/cli/plugins.rb +54 -4
- data/lib/kward/cli/prompt_interface.rb +111 -6
- data/lib/kward/cli/rendering.rb +11 -6
- data/lib/kward/cli/runtime_helpers.rb +133 -3
- data/lib/kward/cli/sessions.rb +262 -13
- data/lib/kward/cli/settings.rb +216 -37
- data/lib/kward/cli/slash_commands.rb +439 -8
- data/lib/kward/cli/tabs.rb +695 -0
- data/lib/kward/cli.rb +171 -26
- data/lib/kward/compactor.rb +4 -1
- data/lib/kward/config_files.rb +125 -5
- data/lib/kward/context_budget_meter.rb +44 -0
- data/lib/kward/conversation.rb +59 -22
- data/lib/kward/editor_mode.rb +25 -0
- data/lib/kward/ekwsh.rb +362 -0
- data/lib/kward/model/client.rb +37 -50
- data/lib/kward/model/context_usage.rb +13 -6
- data/lib/kward/model/model_info.rb +92 -16
- data/lib/kward/model/payloads.rb +2 -0
- data/lib/kward/openrouter_model_cache.rb +120 -0
- data/lib/kward/plugin_registry.rb +108 -1
- data/lib/kward/project_files.rb +52 -0
- data/lib/kward/prompt_history.rb +82 -0
- data/lib/kward/prompt_interface/banner.rb +16 -51
- data/lib/kward/prompt_interface/composer_controller.rb +124 -83
- data/lib/kward/prompt_interface/composer_renderer.rb +116 -14
- 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 +416 -43
- data/lib/kward/prompt_interface/layout.rb +2 -2
- 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/prompt_renderer.rb +32 -13
- data/lib/kward/prompt_interface/question_prompt.rb +122 -82
- data/lib/kward/prompt_interface/runtime_state.rb +49 -1
- data/lib/kward/prompt_interface/screen.rb +17 -0
- data/lib/kward/prompt_interface/selection_prompt.rb +511 -58
- data/lib/kward/prompt_interface/stream_state.rb +7 -0
- data/lib/kward/prompt_interface/transcript_buffer.rb +13 -16
- data/lib/kward/prompt_interface/transcript_renderer.rb +3 -3
- data/lib/kward/prompt_interface.rb +307 -35
- data/lib/kward/prompts/commands.rb +7 -1
- data/lib/kward/prompts.rb +4 -2
- data/lib/kward/rpc/server.rb +45 -11
- data/lib/kward/rpc/session_manager.rb +52 -53
- 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 +67 -4
- 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/telemetry/logger.rb +5 -3
- data/lib/kward/text_boundary.rb +25 -0
- data/lib/kward/tool_output_compactor.rb +127 -0
- data/lib/kward/tools/base.rb +8 -2
- 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 +92 -15
- data/lib/kward/tools/retrieve_tool_output.rb +71 -0
- data/lib/kward/tools/search/web.rb +2 -2
- data/lib/kward/tools/summarize_file_structure.rb +29 -0
- data/lib/kward/tools/tool_call.rb +12 -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 +154 -12
- data/templates/default/fulldoc/html/css/kward.css +362 -42
- data/templates/default/fulldoc/html/full_list.erb +107 -0
- data/templates/default/fulldoc/html/js/kward.js +161 -2
- data/templates/default/fulldoc/html/setup.rb +8 -0
- data/templates/default/kward_navigation.rb +102 -0
- data/templates/default/layout/html/layout.erb +43 -10
- data/templates/default/layout/html/setup.rb +39 -38
- metadata +65 -3
- data/lib/kward/resources/avatar_kward_logo.rb +0 -50
- data/lib/kward/resources/pixel_logo.rb +0 -232
|
@@ -10,10 +10,15 @@ module Kward
|
|
|
10
10
|
@limit = limit
|
|
11
11
|
@text = +""
|
|
12
12
|
@display_rows_cache_width = nil
|
|
13
|
-
@display_rows_cache_banner_count = nil
|
|
14
13
|
@display_rows_cache = nil
|
|
15
14
|
end
|
|
16
15
|
|
|
16
|
+
def initialize_copy(source)
|
|
17
|
+
super
|
|
18
|
+
@text = source.text.dup
|
|
19
|
+
invalidate_display_rows_cache
|
|
20
|
+
end
|
|
21
|
+
|
|
17
22
|
def to_s
|
|
18
23
|
@text
|
|
19
24
|
end
|
|
@@ -42,30 +47,23 @@ module Kward
|
|
|
42
47
|
@text
|
|
43
48
|
end
|
|
44
49
|
|
|
45
|
-
def viewport_text(row_count, width
|
|
46
|
-
viewport_rows(row_count, width
|
|
50
|
+
def viewport_text(row_count, width)
|
|
51
|
+
viewport_rows(row_count, width).join("\n")
|
|
47
52
|
end
|
|
48
53
|
|
|
49
|
-
def viewport_rows(row_count, width
|
|
54
|
+
def viewport_rows(row_count, width)
|
|
50
55
|
return [] unless row_count.positive?
|
|
51
56
|
|
|
52
|
-
rows = display_rows(width
|
|
57
|
+
rows = display_rows(width).last(row_count)
|
|
53
58
|
rows = ([""] * (row_count - rows.length)) + rows if rows.length < row_count
|
|
54
59
|
rows
|
|
55
60
|
end
|
|
56
61
|
|
|
57
|
-
def display_rows(width
|
|
58
|
-
if @display_rows_cache_width == width && @
|
|
59
|
-
return @display_rows_cache
|
|
60
|
-
end
|
|
62
|
+
def display_rows(width)
|
|
63
|
+
return @display_rows_cache if @display_rows_cache_width == width && @display_rows_cache
|
|
61
64
|
|
|
62
|
-
rows = []
|
|
63
|
-
visual_banner_count.times { rows.concat(banner_rows.call(width)) }
|
|
64
|
-
rows << "" if visual_banner_count.positive? && @text.empty?
|
|
65
|
-
rows.concat(text_display_rows(width))
|
|
66
65
|
@display_rows_cache_width = width
|
|
67
|
-
@
|
|
68
|
-
@display_rows_cache = rows
|
|
66
|
+
@display_rows_cache = text_display_rows(width)
|
|
69
67
|
end
|
|
70
68
|
|
|
71
69
|
def text_display_rows(width)
|
|
@@ -77,7 +75,6 @@ module Kward
|
|
|
77
75
|
|
|
78
76
|
def invalidate_display_rows_cache
|
|
79
77
|
@display_rows_cache_width = nil
|
|
80
|
-
@display_rows_cache_banner_count = nil
|
|
81
78
|
@display_rows_cache = nil
|
|
82
79
|
end
|
|
83
80
|
end
|
|
@@ -11,7 +11,7 @@ module Kward
|
|
|
11
11
|
prepare_transcript_output_locked unless @restoring_transcript
|
|
12
12
|
if label && @stream_state.block != label
|
|
13
13
|
ensure_transcript_block_separator_locked
|
|
14
|
-
write_transcript_text_locked("#{colored("#{transcript_label(label)}>", *label_styles(label))}
|
|
14
|
+
write_transcript_text_locked("#{colored("#{transcript_label(label)}>", *label_styles(label))} ")
|
|
15
15
|
@stream_state.start_block(label)
|
|
16
16
|
end
|
|
17
17
|
write_transcript_text_locked(delta) unless delta.empty?
|
|
@@ -82,11 +82,11 @@ module Kward
|
|
|
82
82
|
end
|
|
83
83
|
|
|
84
84
|
def transcript_renderable?
|
|
85
|
-
|
|
85
|
+
!@transcript_buffer.empty?
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def transcript_display_rows(width)
|
|
89
|
-
@transcript_buffer.display_rows(width
|
|
89
|
+
@transcript_buffer.display_rows(width)
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
def transcript_text_display_rows(width)
|
|
@@ -1,21 +1,44 @@
|
|
|
1
|
+
require "base64"
|
|
2
|
+
require "find"
|
|
1
3
|
require "io/console"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "rbconfig"
|
|
2
6
|
require "thread"
|
|
7
|
+
require_relative "project_files"
|
|
8
|
+
require_relative "prompt_history"
|
|
3
9
|
require "tty-cursor"
|
|
4
10
|
require "tty-reader"
|
|
5
11
|
require "tty-screen"
|
|
6
12
|
require_relative "ansi"
|
|
13
|
+
require_relative "editor_mode"
|
|
7
14
|
require_relative "prompt_interface/banner"
|
|
8
15
|
require_relative "prompt_interface/composer_state"
|
|
16
|
+
require_relative "prompt_interface/editor/state"
|
|
9
17
|
require_relative "prompt_interface/transcript_buffer"
|
|
10
18
|
require_relative "prompt_interface/transcript_renderer"
|
|
11
19
|
require_relative "prompt_interface/prompt_renderer"
|
|
12
20
|
require_relative "prompt_interface/stream_state"
|
|
13
21
|
require_relative "prompt_interface/slash_overlay"
|
|
22
|
+
require_relative "prompt_interface/file_overlay"
|
|
23
|
+
require_relative "prompt_interface/project_browser"
|
|
14
24
|
require_relative "prompt_interface/selection_prompt"
|
|
15
25
|
require_relative "prompt_interface/question_prompt"
|
|
26
|
+
require_relative "prompt_interface/git_prompt"
|
|
16
27
|
require_relative "prompt_interface/overlay_renderer"
|
|
28
|
+
require_relative "prompt_interface/editor/renderer"
|
|
29
|
+
require_relative "prompt_interface/editor/syntax_highlighter"
|
|
30
|
+
require_relative "prompt_interface/editor/auto_close_pairs"
|
|
31
|
+
require_relative "prompt_interface/editor/endwise"
|
|
32
|
+
require_relative "prompt_interface/editor/auto_indent"
|
|
17
33
|
require_relative "prompt_interface/composer_renderer"
|
|
18
34
|
require_relative "prompt_interface/composer_controller"
|
|
35
|
+
require_relative "prompt_interface/editor/modes/modern"
|
|
36
|
+
require_relative "prompt_interface/editor/modes/emacs"
|
|
37
|
+
require_relative "prompt_interface/editor/modes/vibe"
|
|
38
|
+
require_relative "prompt_interface/editor/controller"
|
|
39
|
+
require_relative "prompt_interface/interactive/controller"
|
|
40
|
+
require_relative "prompt_interface/interactive/renderer"
|
|
41
|
+
require_relative "prompt_interface/interactive/state"
|
|
19
42
|
require_relative "prompt_interface/layout"
|
|
20
43
|
require_relative "prompt_interface/screen"
|
|
21
44
|
require_relative "prompt_interface/key_handler"
|
|
@@ -39,24 +62,38 @@ module Kward
|
|
|
39
62
|
SPINNER_FRAMES = %w[⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏].freeze
|
|
40
63
|
SPINNER_INTERVAL = 0.1
|
|
41
64
|
FOOTER_REFRESH_INTERVAL = 1.0
|
|
65
|
+
COMPOSER_STATUS_REFRESH_INTERVAL = 1.0
|
|
42
66
|
COMPOSER_MAX_INPUT_ROWS = 6
|
|
43
67
|
TRANSCRIPT_BUFFER_LIMIT = 200_000
|
|
44
|
-
BANNER_LOGO_PIXELS = Banner::LOGO_PIXELS
|
|
45
68
|
BANNER_MESSAGE = Banner::MESSAGE
|
|
46
69
|
|
|
47
70
|
include SlashOverlay
|
|
71
|
+
include FileOverlay
|
|
72
|
+
include ProjectBrowser
|
|
48
73
|
include SelectionPrompt
|
|
49
74
|
include QuestionPrompt
|
|
75
|
+
include GitPrompt
|
|
50
76
|
include OverlayRenderer
|
|
77
|
+
include EditorRenderer
|
|
78
|
+
include EditorSyntaxHighlighter
|
|
79
|
+
include EditorAutoClosePairs
|
|
80
|
+
include EditorEndwise
|
|
81
|
+
include EditorAutoIndent
|
|
51
82
|
include ComposerRenderer
|
|
52
83
|
include ComposerController
|
|
84
|
+
include ModernEditorMode
|
|
85
|
+
include EmacsEditorMode
|
|
86
|
+
include VibeEditorMode
|
|
87
|
+
include EditorController
|
|
88
|
+
include InteractiveRenderer
|
|
89
|
+
include InteractiveState
|
|
53
90
|
include Layout
|
|
54
91
|
include Screen
|
|
55
92
|
include KeyHandler
|
|
56
93
|
include RuntimeState
|
|
57
94
|
include TranscriptRenderer
|
|
58
95
|
include PromptRenderer
|
|
59
|
-
KEYBOARD_PROTOCOL_ENABLE = "\e[>
|
|
96
|
+
KEYBOARD_PROTOCOL_ENABLE = "\e[>25u".freeze
|
|
60
97
|
KEYBOARD_PROTOCOL_RESTORE = "\e[<u".freeze
|
|
61
98
|
BRACKETED_PASTE_ENABLE = "\e[?2004h".freeze
|
|
62
99
|
BRACKETED_PASTE_RESTORE = "\e[?2004l".freeze
|
|
@@ -66,10 +103,14 @@ module Kward
|
|
|
66
103
|
SYNCHRONIZED_OUTPUT_DISABLE = "\e[?2026l".freeze
|
|
67
104
|
CURSOR_SHOW = "\e[?25h".freeze
|
|
68
105
|
CURSOR_HIDE = "\e[?25l".freeze
|
|
106
|
+
CURSOR_SHAPE_DEFAULT = "\e[0 q".freeze
|
|
107
|
+
CURSOR_SHAPE_BAR = "\e[6 q".freeze
|
|
69
108
|
SHIFT_ENTER_SEQUENCES = ["\e[13;2u", "\e[13;2~", "\e[27;2;13~", "\e\r", "\e\n"].freeze
|
|
70
109
|
EXIT_INPUT = :exit_input
|
|
71
110
|
CANCEL_INPUT = :cancel_input
|
|
72
111
|
SELECT_CANCEL = :select_cancel
|
|
112
|
+
SELECT_CONTINUE = :select_continue
|
|
113
|
+
SELECT_ACTION_MINIMUM_BUSY_SECONDS = 1.0
|
|
73
114
|
|
|
74
115
|
# Submitted input string carrying optional display text for transcripts.
|
|
75
116
|
class SubmittedInput < String
|
|
@@ -81,12 +122,14 @@ module Kward
|
|
|
81
122
|
end
|
|
82
123
|
end
|
|
83
124
|
|
|
84
|
-
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil,
|
|
125
|
+
def initialize(input: $stdin, output: $stdout, slash_commands: [], overlay_settings: nil, footer: nil, composer_status: nil, busy_help: true, attachment_badges: nil, attachment_parser: nil, banner_message: nil, tab_keybindings: nil, prompt_history: nil, editor_mode: nil, editor_mode_source: nil, editor_auto_indent: true, editor_auto_indent_source: nil, editor_auto_close_pairs: true, editor_auto_close_pairs_source: nil, editor_soft_wrap: true, editor_soft_wrap_source: nil, editor_bar_cursor: true, editor_bar_cursor_source: nil, editor_line_numbers: "absolute", editor_line_numbers_source: nil)
|
|
85
126
|
@input_io = input
|
|
86
127
|
@output_io = output
|
|
87
128
|
@reader = TTY::Reader.new(input: input, output: output, interrupt: :error)
|
|
88
129
|
@mutex = Mutex.new
|
|
130
|
+
@prompt_history = prompt_history
|
|
89
131
|
@composer = ComposerState.new
|
|
132
|
+
load_history(@prompt_history.values) if @prompt_history
|
|
90
133
|
self.composer_input = @composer.input
|
|
91
134
|
self.composer_cursor = @composer.cursor
|
|
92
135
|
@started = false
|
|
@@ -98,6 +141,8 @@ module Kward
|
|
|
98
141
|
@spinner_frame_index = 0
|
|
99
142
|
@last_spinner_tick = monotonic_now
|
|
100
143
|
@last_footer_refresh = monotonic_now
|
|
144
|
+
@last_composer_status_refresh = 0.0
|
|
145
|
+
@cached_composer_status_text = nil
|
|
101
146
|
@prompt_label = "You>"
|
|
102
147
|
@assistant_label = "Assistant"
|
|
103
148
|
@stream_state = StreamState.new
|
|
@@ -105,22 +150,35 @@ module Kward
|
|
|
105
150
|
@last_composer_rows = []
|
|
106
151
|
@cursor_rendered_row = 0
|
|
107
152
|
@transcript_buffer = TranscriptBuffer.new(limit: TRANSCRIPT_BUFFER_LIMIT)
|
|
108
|
-
@visual_banner_count = 0
|
|
109
153
|
@transcript_viewport_rows = 0
|
|
110
154
|
@restoring_transcript = false
|
|
111
155
|
@pending_keys = []
|
|
156
|
+
@completion_provider = nil
|
|
112
157
|
@original_console_mode = nil
|
|
113
158
|
@raw_mode_active = false
|
|
114
159
|
@slash_commands = normalize_slash_commands(slash_commands)
|
|
115
160
|
@slash_selection_index = 0
|
|
116
161
|
@slash_overlay_dismissed_input = nil
|
|
162
|
+
@file_selection_index = 0
|
|
163
|
+
@file_overlay_dismissed_token = nil
|
|
164
|
+
@file_open_dismissed_token = nil
|
|
165
|
+
@file_editor_open_status = nil
|
|
166
|
+
@file_mention_paths = nil
|
|
167
|
+
@project_browser_state = nil
|
|
168
|
+
@project_browser_restore_after_editor = false
|
|
169
|
+
@editor_state = nil
|
|
170
|
+
@interactive_state = nil
|
|
171
|
+
@last_interactive_tick = monotonic_now
|
|
117
172
|
@select_state = nil
|
|
118
173
|
@question_state = nil
|
|
174
|
+
@question_prompt_active = false
|
|
175
|
+
@git_state = nil
|
|
119
176
|
@last_width = screen_width
|
|
120
177
|
@last_height = screen_height
|
|
121
178
|
@reserved_rows = 0
|
|
122
179
|
@color_enabled = ANSI.enabled?(output)
|
|
123
180
|
@cursor_visible = true
|
|
181
|
+
@editor_bar_cursor_active = false
|
|
124
182
|
@synchronized_output_depth = 0
|
|
125
183
|
@overlay_settings = normalize_overlay_settings(overlay_settings)
|
|
126
184
|
@footer = footer
|
|
@@ -128,7 +186,22 @@ module Kward
|
|
|
128
186
|
@busy_help = busy_help
|
|
129
187
|
@attachment_badges = attachment_badges
|
|
130
188
|
@attachment_parser = attachment_parser
|
|
131
|
-
@banner = Banner.new(message: banner_message,
|
|
189
|
+
@banner = Banner.new(message: banner_message, screen_height: method(:screen_height))
|
|
190
|
+
@tabs = []
|
|
191
|
+
@active_tab_index = 0
|
|
192
|
+
@tab_keybindings = normalize_tab_keybindings(tab_keybindings)
|
|
193
|
+
@editor_mode = normalize_editor_mode(editor_mode)
|
|
194
|
+
@editor_mode_source = editor_mode_source
|
|
195
|
+
@editor_auto_indent = editor_auto_indent != false
|
|
196
|
+
@editor_auto_indent_source = editor_auto_indent_source
|
|
197
|
+
@editor_auto_close_pairs = editor_auto_close_pairs != false
|
|
198
|
+
@editor_auto_close_pairs_source = editor_auto_close_pairs_source
|
|
199
|
+
@editor_soft_wrap = editor_soft_wrap != false
|
|
200
|
+
@editor_soft_wrap_source = editor_soft_wrap_source
|
|
201
|
+
@editor_bar_cursor = editor_bar_cursor != false
|
|
202
|
+
@editor_bar_cursor_source = editor_bar_cursor_source
|
|
203
|
+
@editor_line_numbers = normalize_editor_line_numbers(editor_line_numbers)
|
|
204
|
+
@editor_line_numbers_source = editor_line_numbers_source
|
|
132
205
|
end
|
|
133
206
|
|
|
134
207
|
def start(render: true)
|
|
@@ -138,6 +211,7 @@ module Kward
|
|
|
138
211
|
enter_raw_mode_locked
|
|
139
212
|
@started = true
|
|
140
213
|
@asking = true
|
|
214
|
+
disable_editor_mouse_reporting(force: true) unless editor_active?
|
|
141
215
|
@output_io.print(KEYBOARD_PROTOCOL_ENABLE)
|
|
142
216
|
@output_io.print(BRACKETED_PASTE_ENABLE)
|
|
143
217
|
render_prompt_locked if render
|
|
@@ -150,8 +224,10 @@ module Kward
|
|
|
150
224
|
|
|
151
225
|
clear_prompt_for_output_locked
|
|
152
226
|
restore_scroll_region_locked
|
|
227
|
+
disable_editor_mouse_reporting(force: true)
|
|
153
228
|
@output_io.print(BRACKETED_PASTE_RESTORE)
|
|
154
229
|
@output_io.print(KEYBOARD_PROTOCOL_RESTORE)
|
|
230
|
+
restore_editor_cursor_shape_locked
|
|
155
231
|
set_cursor_visible_locked(true, force: true)
|
|
156
232
|
@output_io.puts
|
|
157
233
|
@output_io.flush
|
|
@@ -203,7 +279,6 @@ module Kward
|
|
|
203
279
|
@output_io.print(SYNCHRONIZED_OUTPUT_ENABLE)
|
|
204
280
|
clear_prompt_for_output_locked unless @rendered_rows.zero? && @last_composer_rows.empty?
|
|
205
281
|
@transcript_buffer.clear
|
|
206
|
-
@visual_banner_count = 0
|
|
207
282
|
@transcript_viewport_rows = 0
|
|
208
283
|
@stream_state.finish_block
|
|
209
284
|
@stream_state.reset
|
|
@@ -221,6 +296,54 @@ module Kward
|
|
|
221
296
|
end
|
|
222
297
|
end
|
|
223
298
|
|
|
299
|
+
def with_completion_provider(provider)
|
|
300
|
+
previous = @completion_provider
|
|
301
|
+
@completion_provider = provider
|
|
302
|
+
yield
|
|
303
|
+
ensure
|
|
304
|
+
@completion_provider = previous
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def editing_file?
|
|
308
|
+
@mutex.synchronize { editor_active? }
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def edit_file(path, base_dir: Dir.pwd, allow_new: true)
|
|
312
|
+
start(render: false)
|
|
313
|
+
opened = @mutex.synchronize do
|
|
314
|
+
open_editor(path, allow_new: allow_new, base_dir: base_dir, restrict_to_workspace: false).tap do
|
|
315
|
+
render_prompt_locked
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
return false unless opened
|
|
319
|
+
|
|
320
|
+
run_editor
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def run_editor
|
|
324
|
+
loop do
|
|
325
|
+
key = read_key(nonblock: true)
|
|
326
|
+
action = nil
|
|
327
|
+
editor_open = @mutex.synchronize do
|
|
328
|
+
if key.nil?
|
|
329
|
+
resized = handle_resize_locked
|
|
330
|
+
footer_refreshed = tick_footer_locked
|
|
331
|
+
render_prompt_locked if resized || footer_refreshed
|
|
332
|
+
else
|
|
333
|
+
result = handle_key(key)
|
|
334
|
+
action = result if prompt_action_result?(result)
|
|
335
|
+
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
|
|
336
|
+
end
|
|
337
|
+
editor_active?
|
|
338
|
+
end
|
|
339
|
+
return action if action
|
|
340
|
+
break unless editor_open
|
|
341
|
+
|
|
342
|
+
sleep 0.02 if key.nil?
|
|
343
|
+
end
|
|
344
|
+
true
|
|
345
|
+
end
|
|
346
|
+
|
|
224
347
|
def ask(message = "You>")
|
|
225
348
|
was_composing = @started && @asking
|
|
226
349
|
start
|
|
@@ -251,10 +374,10 @@ module Kward
|
|
|
251
374
|
render_prompt_locked if resized || footer_refreshed
|
|
252
375
|
else
|
|
253
376
|
result = handle_key(key)
|
|
254
|
-
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT
|
|
377
|
+
render_prompt_locked unless result.is_a?(String) || result == EXIT_INPUT || prompt_action_result?(result)
|
|
255
378
|
end
|
|
256
379
|
end
|
|
257
|
-
return result if result.is_a?(String)
|
|
380
|
+
return result if result.is_a?(String) || prompt_action_result?(result)
|
|
258
381
|
return nil if result == EXIT_INPUT
|
|
259
382
|
|
|
260
383
|
sleep 0.02 if key.nil?
|
|
@@ -275,23 +398,15 @@ module Kward
|
|
|
275
398
|
[overlay_card_width(screen_width) - 6, 1].max
|
|
276
399
|
end
|
|
277
400
|
|
|
278
|
-
def select(message, choices, title: "Sessions", custom: false, initial_index: 0)
|
|
401
|
+
def select(message, choices, title: "Sessions", custom: false, initial_index: 0, action_keys: {}, action_handlers: {})
|
|
279
402
|
return nil if choices.empty? && !custom
|
|
280
403
|
|
|
281
404
|
start
|
|
282
405
|
@mutex.synchronize do
|
|
283
|
-
|
|
284
|
-
self.composer_input = ""
|
|
285
|
-
self.composer_cursor = 0
|
|
286
|
-
@composer.clear_attachments
|
|
287
|
-
@pending_keys.clear
|
|
288
|
-
@asking = true
|
|
289
|
-
@busy = false
|
|
290
|
-
@queued_count = 0
|
|
406
|
+
prepare_modal_input_locked(message, clear_attachments: true)
|
|
291
407
|
choice_labels = choices.map(&:to_s)
|
|
292
408
|
selection_index = choice_labels.empty? ? 0 : [[initial_index.to_i, 0].max, choice_labels.length - 1].min
|
|
293
|
-
@select_state = { choices: choice_labels, selection_index: selection_index, title: title.to_s, custom: custom }
|
|
294
|
-
reset_history_navigation
|
|
409
|
+
@select_state = { choices: choice_labels, selection_index: selection_index, title: title.to_s, custom: custom, action_keys: normalized_select_action_keys(action_keys), search_active: false }
|
|
295
410
|
render_prompt_locked
|
|
296
411
|
end
|
|
297
412
|
|
|
@@ -305,12 +420,20 @@ module Kward
|
|
|
305
420
|
render_prompt_locked if resized || footer_refreshed
|
|
306
421
|
else
|
|
307
422
|
result = handle_select_key(key)
|
|
308
|
-
|
|
423
|
+
result = drain_pending_select_keys_locked(result)
|
|
424
|
+
render_prompt_locked unless result.is_a?(String) || select_action_result?(result) || result == SELECT_CANCEL
|
|
309
425
|
end
|
|
310
426
|
end
|
|
311
427
|
|
|
312
|
-
if
|
|
313
|
-
|
|
428
|
+
if select_action_result?(result) && select_action_handler(result, action_handlers)
|
|
429
|
+
action_result = run_select_action(result, select_action_handler(result, action_handlers))
|
|
430
|
+
next if action_result == SELECT_CONTINUE
|
|
431
|
+
|
|
432
|
+
return action_result
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
if result.is_a?(String) || select_action_result?(result) || result == SELECT_CANCEL
|
|
436
|
+
finish_select_prompt(render: !select_deferred_finish_render?(result))
|
|
314
437
|
return result == SELECT_CANCEL ? nil : result
|
|
315
438
|
end
|
|
316
439
|
|
|
@@ -324,7 +447,10 @@ module Kward
|
|
|
324
447
|
start
|
|
325
448
|
saved_state = nil
|
|
326
449
|
answers = []
|
|
327
|
-
@mutex.synchronize
|
|
450
|
+
@mutex.synchronize do
|
|
451
|
+
@question_prompt_active = true
|
|
452
|
+
saved_state = begin_question_prompt_state
|
|
453
|
+
end
|
|
328
454
|
|
|
329
455
|
questions.each_with_index do |question, index|
|
|
330
456
|
answer = ask_single_user_question(question, index + 1, questions.length)
|
|
@@ -343,7 +469,127 @@ module Kward
|
|
|
343
469
|
end
|
|
344
470
|
|
|
345
471
|
def modal_active?
|
|
346
|
-
@mutex.synchronize {
|
|
472
|
+
@mutex.synchronize { modal_active_locked? }
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def interactive_active?
|
|
476
|
+
@mutex.synchronize { interactive_active_locked? }
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
def interactive_exited?
|
|
480
|
+
@mutex.synchronize do
|
|
481
|
+
return false unless @interactive_state
|
|
482
|
+
|
|
483
|
+
@interactive_state[:controller].exited?
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def finish_interactive
|
|
488
|
+
@mutex.synchronize do
|
|
489
|
+
return unless @interactive_state
|
|
490
|
+
|
|
491
|
+
snapshot = @interactive_state[:snapshot]
|
|
492
|
+
@interactive_state = nil
|
|
493
|
+
restore_composer_snapshot_locked(snapshot)
|
|
494
|
+
redraw_screen_locked if @started
|
|
495
|
+
@output_io.flush
|
|
496
|
+
end
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def start_interactive(title:, rows:, fps:)
|
|
500
|
+
snapshot = composer_snapshot
|
|
501
|
+
controller = InteractiveController.new(width: interactive_canvas_width, height: rows, fps: fps)
|
|
502
|
+
start
|
|
503
|
+
@mutex.synchronize do
|
|
504
|
+
@interactive_state = {
|
|
505
|
+
title: title.to_s,
|
|
506
|
+
rows: rows,
|
|
507
|
+
controller: controller,
|
|
508
|
+
snapshot: snapshot
|
|
509
|
+
}
|
|
510
|
+
@last_interactive_tick = monotonic_now
|
|
511
|
+
@asking = true
|
|
512
|
+
@busy = false
|
|
513
|
+
@last_composer_rows = []
|
|
514
|
+
render_prompt_locked
|
|
515
|
+
end
|
|
516
|
+
controller
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
def update_tabs(labels:, active_index: 0)
|
|
520
|
+
@mutex.synchronize do
|
|
521
|
+
@tabs = Array(labels).map { |label| normalize_tab_label(label) }
|
|
522
|
+
@active_tab_index = active_index.to_i
|
|
523
|
+
render_prompt_locked if @started && @asking
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def composer_snapshot
|
|
528
|
+
@mutex.synchronize do
|
|
529
|
+
{
|
|
530
|
+
composer: @composer,
|
|
531
|
+
prompt_label: @prompt_label
|
|
532
|
+
}
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def tab_view_snapshot
|
|
537
|
+
@mutex.synchronize do
|
|
538
|
+
{
|
|
539
|
+
composer: @composer.dup,
|
|
540
|
+
prompt_label: @prompt_label.dup,
|
|
541
|
+
editor_state: @editor_state&.dup,
|
|
542
|
+
transcript_buffer: @transcript_buffer.dup,
|
|
543
|
+
transcript_viewport_rows: @transcript_viewport_rows,
|
|
544
|
+
stream_state: @stream_state.dup
|
|
545
|
+
}
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def restore_composer_snapshot(snapshot)
|
|
550
|
+
@mutex.synchronize do
|
|
551
|
+
restore_composer_snapshot_locked(snapshot)
|
|
552
|
+
restore_editor_snapshot_locked(snapshot)
|
|
553
|
+
redraw_screen_locked if @started
|
|
554
|
+
end
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def restore_tab_view_snapshot(snapshot)
|
|
558
|
+
@mutex.synchronize do
|
|
559
|
+
restore_composer_snapshot_locked(snapshot)
|
|
560
|
+
restore_editor_snapshot_locked(snapshot)
|
|
561
|
+
@transcript_buffer = snapshot[:transcript_buffer] || TranscriptBuffer.new(limit: TRANSCRIPT_BUFFER_LIMIT)
|
|
562
|
+
@transcript_viewport_rows = snapshot[:transcript_viewport_rows].to_i
|
|
563
|
+
@stream_state = snapshot[:stream_state] || StreamState.new
|
|
564
|
+
@last_composer_rows = []
|
|
565
|
+
redraw_screen_locked if @started
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def restore_composer_snapshot_locked(snapshot)
|
|
570
|
+
@composer = snapshot[:composer] || new_composer_state_with_history
|
|
571
|
+
@prompt_label = snapshot[:prompt_label].to_s.empty? ? "You>" : snapshot[:prompt_label].to_s
|
|
572
|
+
self.composer_input = @composer.input
|
|
573
|
+
self.composer_cursor = @composer.cursor
|
|
574
|
+
@last_composer_rows = []
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
def new_composer_state_with_history
|
|
578
|
+
composer = ComposerState.new
|
|
579
|
+
composer.load_history(@prompt_history.values) if @prompt_history
|
|
580
|
+
composer
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def restore_editor_snapshot_locked(snapshot)
|
|
584
|
+
editor_was_active = editor_active?
|
|
585
|
+
@editor_state = snapshot[:editor_state]&.dup
|
|
586
|
+
editor_is_active = editor_active?
|
|
587
|
+
|
|
588
|
+
if editor_is_active
|
|
589
|
+
enable_editor_mouse_reporting unless editor_was_active
|
|
590
|
+
else
|
|
591
|
+
disable_editor_mouse_reporting(force: true)
|
|
592
|
+
end
|
|
347
593
|
end
|
|
348
594
|
|
|
349
595
|
def update_overlay_settings(settings)
|
|
@@ -410,6 +656,22 @@ module Kward
|
|
|
410
656
|
def poll_input
|
|
411
657
|
key = read_key(nonblock: true)
|
|
412
658
|
@mutex.synchronize do
|
|
659
|
+
if interactive_active_locked?
|
|
660
|
+
if key.nil?
|
|
661
|
+
resized = handle_resize_locked
|
|
662
|
+
ticked = tick_interactive_locked
|
|
663
|
+
render_prompt_locked if resized || ticked
|
|
664
|
+
return :interactive_exited if @interactive_state[:controller].exited?
|
|
665
|
+
return nil
|
|
666
|
+
end
|
|
667
|
+
|
|
668
|
+
route_interactive_key(key)
|
|
669
|
+
ticked = tick_interactive_locked
|
|
670
|
+
render_prompt_locked if ticked
|
|
671
|
+
return :interactive_exited if @interactive_state[:controller].exited?
|
|
672
|
+
return nil
|
|
673
|
+
end
|
|
674
|
+
|
|
413
675
|
if key.nil?
|
|
414
676
|
resized = handle_resize_locked
|
|
415
677
|
spun = tick_spinner_locked
|
|
@@ -418,8 +680,13 @@ module Kward
|
|
|
418
680
|
return nil
|
|
419
681
|
end
|
|
420
682
|
|
|
683
|
+
if modal_active_locked?
|
|
684
|
+
queue_pending_keys(key)
|
|
685
|
+
return nil
|
|
686
|
+
end
|
|
687
|
+
|
|
421
688
|
result = handle_key(key)
|
|
422
|
-
render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result)
|
|
689
|
+
render_prompt_locked unless [EXIT_INPUT, CANCEL_INPUT].include?(result) || prompt_action_result?(result)
|
|
423
690
|
[EXIT_INPUT, CANCEL_INPUT].include?(result) ? result : result
|
|
424
691
|
end
|
|
425
692
|
end
|
|
@@ -430,20 +697,16 @@ module Kward
|
|
|
430
697
|
end
|
|
431
698
|
end
|
|
432
699
|
|
|
433
|
-
def print_visual_banner
|
|
700
|
+
def print_visual_banner(message = nil)
|
|
434
701
|
@mutex.synchronize do
|
|
435
702
|
width, height = screen_size
|
|
436
|
-
rows = banner_rows(width)
|
|
703
|
+
rows = banner_rows(width, message: message)
|
|
437
704
|
return if rows.empty?
|
|
438
705
|
|
|
439
706
|
with_synchronized_output_locked do
|
|
440
707
|
prepare_transcript_output_locked
|
|
441
|
-
rows.
|
|
442
|
-
|
|
443
|
-
write_visual_transcript_text_locked("\n")
|
|
444
|
-
end
|
|
445
|
-
@visual_banner_count += 1
|
|
446
|
-
invalidate_transcript_display_rows_cache
|
|
708
|
+
write_transcript_text_locked(rows.join("\n"))
|
|
709
|
+
write_transcript_text_locked("\n")
|
|
447
710
|
remember_transcript_viewport_locked(height)
|
|
448
711
|
@stream_state.finish_block
|
|
449
712
|
restore_composer_cursor_locked
|
|
@@ -484,10 +747,17 @@ module Kward
|
|
|
484
747
|
end
|
|
485
748
|
end
|
|
486
749
|
|
|
750
|
+
def refresh_composer_status
|
|
751
|
+
@mutex.synchronize do
|
|
752
|
+
@cached_composer_status_text = nil
|
|
753
|
+
@last_composer_status_refresh = 0.0
|
|
754
|
+
render_prompt_locked if @started && @asking
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
487
758
|
def clear_transcript
|
|
488
759
|
@mutex.synchronize do
|
|
489
760
|
@transcript_buffer.clear
|
|
490
|
-
@visual_banner_count = 0
|
|
491
761
|
@transcript_viewport_rows = 0
|
|
492
762
|
@stream_state.finish_block
|
|
493
763
|
@stream_state.reset
|
|
@@ -499,7 +769,9 @@ module Kward
|
|
|
499
769
|
|
|
500
770
|
private
|
|
501
771
|
|
|
502
|
-
|
|
772
|
+
def modal_active_locked?
|
|
773
|
+
@question_prompt_active || !@question_state.nil? || !@select_state.nil? || !@git_state.nil?
|
|
774
|
+
end
|
|
503
775
|
|
|
504
776
|
|
|
505
777
|
|
|
@@ -11,7 +11,9 @@ module Kward
|
|
|
11
11
|
{ name: "sessions", description: "Open the saved sessions picker.", argument_hint: "[path]" },
|
|
12
12
|
{ name: "resume", description: "Alias for /sessions.", argument_hint: "[path]" },
|
|
13
13
|
{ name: "name", description: "Name or clear the current session.", argument_hint: "[name]" },
|
|
14
|
+
{ name: "rename", description: "Rename the current session.", argument_hint: "<name>" },
|
|
14
15
|
{ name: "clone", description: "Clone the current session.", argument_hint: "" },
|
|
16
|
+
{ name: "fork", description: "Fork from an earlier prompt into a new session.", argument_hint: "" },
|
|
15
17
|
{ name: "rewind", description: "Revisit an earlier prompt and try a different direction.", argument_hint: "" },
|
|
16
18
|
{ name: "tree", description: "Inspect and navigate the full technical session tree.", argument_hint: "" },
|
|
17
19
|
{ name: "copy", description: "Copy clean session text to the clipboard.", argument_hint: "[last|transcript]" },
|
|
@@ -21,9 +23,13 @@ module Kward
|
|
|
21
23
|
{ name: "settings", description: "Configure prompt overlays.", argument_hint: "" },
|
|
22
24
|
{ name: "login", description: "Log in with an OAuth provider.", argument_hint: "" },
|
|
23
25
|
{ name: "model", description: "Select the default model.", argument_hint: "" },
|
|
24
|
-
{ name: "openrouter/catalog", description: "List the full OpenRouter model catalog.", argument_hint: "" },
|
|
25
26
|
{ name: "reasoning", description: "Select reasoning effort.", argument_hint: "" },
|
|
26
27
|
{ name: "reload", description: "Reload installed plugins.", argument_hint: "" },
|
|
28
|
+
{ name: "workers", description: "Open the worker pipeline.", argument_hint: "[new|do <task>]" },
|
|
29
|
+
{ name: "git", description: "Review uncommitted changes and commit them.", argument_hint: "" },
|
|
30
|
+
{ name: "files", description: "Browse project files.", argument_hint: "" },
|
|
31
|
+
{ name: "shell", description: "Open the embedded Kward shell.", argument_hint: "" },
|
|
32
|
+
{ name: "tab", description: "Manage tabs.", argument_hint: "[1-n|move|close|new|name]" },
|
|
27
33
|
{ name: "status", description: "Show the current status message.", argument_hint: "" },
|
|
28
34
|
{ name: "stats", description: "Show telemetry logging stats.", argument_hint: "[range]" },
|
|
29
35
|
{ name: "memory", description: "Inspect and manage Kward memory.", argument_hint: "[enable|disable|auto-summary|core|add|list|forget|promote|relax|inspect|why|summarize]" }
|