openclacky 0.7.0 → 0.7.2
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/.clacky/skills/commit/SKILL.md +29 -4
- data/.clackyrules +3 -1
- data/CHANGELOG.md +103 -2
- data/README.md +70 -161
- data/bin/clarky +11 -0
- data/docs/HOW-TO-USE-CN.md +96 -0
- data/docs/HOW-TO-USE.md +94 -0
- data/docs/config.example.yml +27 -0
- data/docs/deploy_subagent_design.md +540 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/why-openclacky.md +0 -1
- data/lib/clacky/agent/cost_tracker.rb +180 -0
- data/lib/clacky/agent/llm_caller.rb +54 -0
- data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
- data/lib/clacky/agent/message_compressor_helper.rb +534 -0
- data/lib/clacky/agent/session_serializer.rb +152 -0
- data/lib/clacky/agent/skill_manager.rb +138 -0
- data/lib/clacky/agent/system_prompt_builder.rb +96 -0
- data/lib/clacky/agent/time_machine.rb +199 -0
- data/lib/clacky/agent/tool_executor.rb +434 -0
- data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
- data/lib/clacky/agent.rb +260 -1370
- data/lib/clacky/agent_config.rb +447 -10
- data/lib/clacky/cli.rb +275 -98
- data/lib/clacky/client.rb +12 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +195 -0
- data/lib/clacky/providers.rb +107 -0
- data/lib/clacky/skill.rb +48 -7
- data/lib/clacky/skill_loader.rb +7 -0
- data/lib/clacky/tools/edit.rb +105 -48
- data/lib/clacky/tools/file_reader.rb +44 -73
- data/lib/clacky/tools/invoke_skill.rb +89 -0
- data/lib/clacky/tools/list_tasks.rb +54 -0
- data/lib/clacky/tools/redo_task.rb +41 -0
- data/lib/clacky/tools/safe_shell.rb +1 -1
- data/lib/clacky/tools/shell.rb +74 -62
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/undo_task.rb +32 -0
- data/lib/clacky/tools/web_fetch.rb +2 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
- data/lib/clacky/ui2/components/inline_input.rb +23 -2
- data/lib/clacky/ui2/components/input_area.rb +65 -21
- data/lib/clacky/ui2/components/modal_component.rb +199 -62
- data/lib/clacky/ui2/layout_manager.rb +75 -25
- data/lib/clacky/ui2/line_editor.rb +23 -2
- data/lib/clacky/ui2/markdown_renderer.rb +31 -10
- data/lib/clacky/ui2/screen_buffer.rb +2 -0
- data/lib/clacky/ui2/ui_controller.rb +316 -37
- data/lib/clacky/ui2.rb +2 -0
- data/lib/clacky/ui_interface.rb +50 -0
- data/lib/clacky/utils/arguments_parser.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -18
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +19 -9
- data/scripts/install.sh +274 -97
- data/scripts/uninstall.sh +12 -12
- metadata +40 -13
- data/.clacky/skills/test-skill/SKILL.md +0 -15
- data/lib/clacky/compression/base.rb +0 -231
- data/lib/clacky/compression/standard.rb +0 -339
- data/lib/clacky/config.rb +0 -117
- /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
- /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
- /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
- /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
- /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
- /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for listing task history (Time Machine feature)
|
|
6
|
+
class ListTasks < Base
|
|
7
|
+
self.tool_name = "list_tasks"
|
|
8
|
+
self.tool_description = "List recent tasks in the task history with summaries. " \
|
|
9
|
+
"Shows current task, past tasks, and future tasks (after undo). " \
|
|
10
|
+
"Use when user wants to see task history or choose which task to undo/redo to."
|
|
11
|
+
self.tool_category = "time_machine"
|
|
12
|
+
self.tool_parameters = {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
limit: {
|
|
16
|
+
type: "integer",
|
|
17
|
+
description: "Maximum number of recent tasks to show (default: 10)",
|
|
18
|
+
default: 10
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def execute(agent:, limit: 10, **_args)
|
|
24
|
+
history = agent.get_task_history(limit: limit)
|
|
25
|
+
|
|
26
|
+
if history.empty?
|
|
27
|
+
return "No task history available."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
lines = ["Task History:"]
|
|
31
|
+
history.each do |task|
|
|
32
|
+
indicator = case task[:status]
|
|
33
|
+
when :current then "→"
|
|
34
|
+
when :past then " "
|
|
35
|
+
when :future then "↯"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
branch_indicator = task[:has_branches] ? " ⎇" : ""
|
|
39
|
+
lines << "#{indicator}#{branch_indicator} Task #{task[:task_id]}: #{task[:summary]}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
lines.join("\n")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_call(limit: 10, **_args)
|
|
46
|
+
"Listing task history (limit: #{limit})..."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def format_result(result)
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for redoing a task after undo (Time Machine feature)
|
|
6
|
+
class RedoTask < Base
|
|
7
|
+
self.tool_name = "redo_task"
|
|
8
|
+
self.tool_description = "Redo to a specific task after undo. Restores files to that task's state. " \
|
|
9
|
+
"Use when user wants to go forward to a future task or switch to a different branch."
|
|
10
|
+
self.tool_category = "time_machine"
|
|
11
|
+
self.tool_parameters = {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
task_id: {
|
|
15
|
+
type: "integer",
|
|
16
|
+
description: "The task ID to redo to (must be greater than current active task)"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
required: ["task_id"]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
def execute(agent:, task_id:, **_args)
|
|
23
|
+
result = agent.switch_to_task(task_id)
|
|
24
|
+
|
|
25
|
+
if result[:success]
|
|
26
|
+
result[:message]
|
|
27
|
+
else
|
|
28
|
+
"Error: #{result[:message]}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_call(task_id:, **_args)
|
|
33
|
+
"Redoing to task #{task_id}..."
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def format_result(result)
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/clacky/tools/shell.rb
CHANGED
|
@@ -66,96 +66,108 @@ module Clacky
|
|
|
66
66
|
stdout_buffer = StringIO.new
|
|
67
67
|
stderr_buffer = StringIO.new
|
|
68
68
|
soft_timeout_triggered = false
|
|
69
|
+
process_pid = nil
|
|
69
70
|
|
|
70
71
|
begin
|
|
71
72
|
Open3.popen3(command) do |stdin, stdout, stderr, wait_thr|
|
|
73
|
+
process_pid = wait_thr.pid
|
|
72
74
|
start_time = Time.now
|
|
73
75
|
|
|
74
76
|
stdout.sync = true
|
|
75
77
|
stderr.sync = true
|
|
76
78
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if elapsed > hard_timeout
|
|
81
|
-
Process.kill('TERM', wait_thr.pid) rescue nil
|
|
82
|
-
sleep 0.5
|
|
83
|
-
Process.kill('KILL', wait_thr.pid) if wait_thr.alive? rescue nil
|
|
84
|
-
|
|
85
|
-
return format_timeout_result(
|
|
86
|
-
command,
|
|
87
|
-
stdout_buffer.string,
|
|
88
|
-
stderr_buffer.string,
|
|
89
|
-
elapsed,
|
|
90
|
-
:hard_timeout,
|
|
91
|
-
hard_timeout,
|
|
92
|
-
max_output_lines
|
|
93
|
-
)
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
if elapsed > soft_timeout && !soft_timeout_triggered
|
|
97
|
-
soft_timeout_triggered = true
|
|
79
|
+
begin
|
|
80
|
+
loop do
|
|
81
|
+
elapsed = Time.now - start_time
|
|
98
82
|
|
|
99
|
-
|
|
100
|
-
interaction = detect_interaction(stdout_buffer.string)
|
|
101
|
-
if interaction
|
|
83
|
+
if elapsed > hard_timeout
|
|
102
84
|
Process.kill('TERM', wait_thr.pid) rescue nil
|
|
103
|
-
|
|
85
|
+
sleep 0.5
|
|
86
|
+
Process.kill('KILL', wait_thr.pid) if wait_thr.alive? rescue nil
|
|
87
|
+
|
|
88
|
+
return format_timeout_result(
|
|
104
89
|
command,
|
|
105
90
|
stdout_buffer.string,
|
|
106
91
|
stderr_buffer.string,
|
|
107
|
-
|
|
92
|
+
elapsed,
|
|
93
|
+
:hard_timeout,
|
|
94
|
+
hard_timeout,
|
|
108
95
|
max_output_lines
|
|
109
96
|
)
|
|
110
97
|
end
|
|
111
|
-
end
|
|
112
98
|
|
|
113
|
-
|
|
99
|
+
if elapsed > soft_timeout && !soft_timeout_triggered
|
|
100
|
+
soft_timeout_triggered = true
|
|
101
|
+
|
|
102
|
+
# L1: Check for interaction patterns
|
|
103
|
+
interaction = detect_interaction(stdout_buffer.string)
|
|
104
|
+
if interaction
|
|
105
|
+
Process.kill('TERM', wait_thr.pid) rescue nil
|
|
106
|
+
return format_waiting_input_result(
|
|
107
|
+
command,
|
|
108
|
+
stdout_buffer.string,
|
|
109
|
+
stderr_buffer.string,
|
|
110
|
+
interaction,
|
|
111
|
+
max_output_lines
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
114
115
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
ready[
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
116
|
+
break unless wait_thr.alive?
|
|
117
|
+
|
|
118
|
+
begin
|
|
119
|
+
ready = IO.select([stdout, stderr], nil, nil, 0.1)
|
|
120
|
+
if ready
|
|
121
|
+
ready[0].each do |io|
|
|
122
|
+
begin
|
|
123
|
+
data = io.read_nonblock(4096)
|
|
124
|
+
if io == stdout
|
|
125
|
+
stdout_buffer.write(data)
|
|
126
|
+
else
|
|
127
|
+
stderr_buffer.write(data)
|
|
128
|
+
end
|
|
129
|
+
rescue IO::WaitReadable, EOFError
|
|
125
130
|
end
|
|
126
|
-
rescue IO::WaitReadable, EOFError
|
|
127
131
|
end
|
|
128
132
|
end
|
|
133
|
+
rescue StandardError => e
|
|
129
134
|
end
|
|
130
|
-
|
|
135
|
+
|
|
136
|
+
sleep 0.1
|
|
131
137
|
end
|
|
132
138
|
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
begin
|
|
140
|
+
stdout_buffer.write(stdout.read)
|
|
141
|
+
rescue StandardError
|
|
142
|
+
end
|
|
143
|
+
begin
|
|
144
|
+
stderr_buffer.write(stderr.read)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
end
|
|
135
147
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
148
|
+
stdout_output = stdout_buffer.string
|
|
149
|
+
stderr_output = stderr_buffer.string
|
|
150
|
+
|
|
151
|
+
{
|
|
152
|
+
command: command,
|
|
153
|
+
stdout: truncate_output(stdout_output, max_output_lines),
|
|
154
|
+
stderr: truncate_output(stderr_output, max_output_lines),
|
|
155
|
+
exit_code: wait_thr.value.exitstatus,
|
|
156
|
+
success: wait_thr.value.success?,
|
|
157
|
+
elapsed: Time.now - start_time,
|
|
158
|
+
output_truncated: output_truncated?(stdout_output, stderr_output, max_output_lines)
|
|
159
|
+
}
|
|
160
|
+
ensure
|
|
161
|
+
# Ensure child process is killed when block exits (for any reason: exception, return, etc.)
|
|
162
|
+
if wait_thr&.alive?
|
|
163
|
+
Process.kill('TERM', wait_thr.pid) rescue nil
|
|
164
|
+
sleep 0.1
|
|
165
|
+
Process.kill('KILL', wait_thr.pid) if wait_thr.alive? rescue nil
|
|
166
|
+
end
|
|
143
167
|
end
|
|
144
|
-
|
|
145
|
-
stdout_output = stdout_buffer.string
|
|
146
|
-
stderr_output = stderr_buffer.string
|
|
147
|
-
|
|
148
|
-
{
|
|
149
|
-
command: command,
|
|
150
|
-
stdout: truncate_output(stdout_output, max_output_lines),
|
|
151
|
-
stderr: truncate_output(stderr_output, max_output_lines),
|
|
152
|
-
exit_code: wait_thr.value.exitstatus,
|
|
153
|
-
success: wait_thr.value.success?,
|
|
154
|
-
elapsed: Time.now - start_time,
|
|
155
|
-
output_truncated: output_truncated?(stdout_output, stderr_output, max_output_lines)
|
|
156
|
-
}
|
|
157
168
|
end
|
|
158
169
|
rescue StandardError => e
|
|
170
|
+
# Handle other errors
|
|
159
171
|
stdout_output = stdout_buffer.string
|
|
160
172
|
stderr_output = "Error executing command: #{e.message}\n#{e.backtrace.first(3).join("\n")}"
|
|
161
173
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for undoing the last task (Time Machine feature)
|
|
6
|
+
class UndoTask < Base
|
|
7
|
+
self.tool_name = "undo_task"
|
|
8
|
+
self.tool_description = "Undo the last task and restore files to previous state. " \
|
|
9
|
+
"Use when user wants to go back to previous state or undo recent changes."
|
|
10
|
+
self.tool_category = "time_machine"
|
|
11
|
+
self.tool_parameters = {}
|
|
12
|
+
|
|
13
|
+
def execute(agent:, **_args)
|
|
14
|
+
result = agent.undo_last_task
|
|
15
|
+
|
|
16
|
+
if result[:success]
|
|
17
|
+
result[:message]
|
|
18
|
+
else
|
|
19
|
+
"Error: #{result[:message]}"
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def format_call(**_args)
|
|
24
|
+
"Undoing last task..."
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def format_result(result)
|
|
28
|
+
result
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -211,7 +211,8 @@ module Clacky
|
|
|
211
211
|
compact[:content] = result[:content]
|
|
212
212
|
compact[:truncated] = true
|
|
213
213
|
compact[:temp_file] = result[:temp_file]
|
|
214
|
-
compact[:message] = "[Content truncated - full content saved to
|
|
214
|
+
compact[:message] = "[Content truncated - full content saved to: #{result[:temp_file]}. " \
|
|
215
|
+
"Use grep to search keywords, or file_reader with start_line/end_line to read sections.]"
|
|
215
216
|
else
|
|
216
217
|
compact[:content] = result[:content]
|
|
217
218
|
compact[:truncated] = result[:truncated] || false
|
|
@@ -14,6 +14,8 @@ module Clacky
|
|
|
14
14
|
# System commands available by default
|
|
15
15
|
SYSTEM_COMMANDS = [
|
|
16
16
|
{ command: "/clear", description: "Clear chat history and restart session" },
|
|
17
|
+
{ command: "/config", description: "Open configuration (models, API keys, settings)" },
|
|
18
|
+
{ command: "/undo", description: "Undo the last task and restore previous state" },
|
|
17
19
|
{ command: "/help", description: "Show help information" },
|
|
18
20
|
{ command: "/exit", description: "Exit the chat session" },
|
|
19
21
|
{ command: "/quit", description: "Quit the application" }
|
|
@@ -46,7 +48,8 @@ module Clacky
|
|
|
46
48
|
{
|
|
47
49
|
command: skill.slash_command,
|
|
48
50
|
description: skill.description || "No description available",
|
|
49
|
-
type: :skill
|
|
51
|
+
type: :skill,
|
|
52
|
+
argument_hint: skill.argument_hint
|
|
50
53
|
}
|
|
51
54
|
end
|
|
52
55
|
|
|
@@ -104,6 +107,13 @@ module Clacky
|
|
|
104
107
|
cmd ? cmd[:command] : nil
|
|
105
108
|
end
|
|
106
109
|
|
|
110
|
+
# Get the argument hint for the currently selected command
|
|
111
|
+
# @return [String, nil] Argument hint string or nil if none
|
|
112
|
+
def selected_argument_hint
|
|
113
|
+
cmd = selected_command
|
|
114
|
+
cmd ? cmd[:argument_hint] : nil
|
|
115
|
+
end
|
|
116
|
+
|
|
107
117
|
# Check if there are any suggestions to show
|
|
108
118
|
# @return [Boolean]
|
|
109
119
|
def has_suggestions?
|
|
@@ -189,8 +199,8 @@ module Clacky
|
|
|
189
199
|
@filtered_commands = @commands.select do |cmd|
|
|
190
200
|
# Remove leading / for comparison
|
|
191
201
|
cmd_name = cmd[:command].sub(/^\//, "")
|
|
192
|
-
|
|
193
|
-
|
|
202
|
+
# Only match command name, not description
|
|
203
|
+
cmd_name.downcase.start_with?(filter_lower)
|
|
194
204
|
end
|
|
195
205
|
end
|
|
196
206
|
end
|
|
@@ -20,6 +20,7 @@ module Clacky
|
|
|
20
20
|
@result_queue = nil
|
|
21
21
|
@paste_counter = 0
|
|
22
22
|
@paste_placeholders = {}
|
|
23
|
+
@continuation_prompt = "> " # Continuation prompt for wrapped lines
|
|
23
24
|
end
|
|
24
25
|
|
|
25
26
|
# Activate inline input and wait for user input
|
|
@@ -92,6 +93,8 @@ module Clacky
|
|
|
92
93
|
{ action: :update }
|
|
93
94
|
when :shift_tab
|
|
94
95
|
handle_shift_tab
|
|
96
|
+
when :ctrl_o
|
|
97
|
+
handle_toggle_expand
|
|
95
98
|
when :ctrl_c
|
|
96
99
|
handle_cancel
|
|
97
100
|
when :escape
|
|
@@ -110,8 +113,10 @@ module Clacky
|
|
|
110
113
|
# @return [String] Rendered line (may wrap to multiple lines)
|
|
111
114
|
def render
|
|
112
115
|
width = TTY::Screen.width
|
|
116
|
+
# Use effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
|
|
117
|
+
content_width = effective_content_width(width)
|
|
113
118
|
prompt_width = calculate_display_width(strip_ansi_codes(@prompt))
|
|
114
|
-
available_width =
|
|
119
|
+
available_width = content_width - prompt_width
|
|
115
120
|
|
|
116
121
|
# Get wrapped segments
|
|
117
122
|
wrapped_segments = wrap_line(@line, available_width)
|
|
@@ -142,12 +147,23 @@ module Clacky
|
|
|
142
147
|
cursor_column(@prompt)
|
|
143
148
|
end
|
|
144
149
|
|
|
150
|
+
# Get cursor position for display (considering line wrapping and continuation prompt)
|
|
151
|
+
# @param width [Integer] Terminal width
|
|
152
|
+
# @return [Array<Integer>] Row and column position (0-indexed)
|
|
153
|
+
def cursor_position_for_display(width = TTY::Screen.width)
|
|
154
|
+
# Use effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
|
|
155
|
+
content_width = effective_content_width(width)
|
|
156
|
+
cursor_position_with_wrap(@prompt, content_width, @continuation_prompt)
|
|
157
|
+
end
|
|
158
|
+
|
|
145
159
|
# Get the number of lines this input will occupy when rendered
|
|
146
160
|
# @param width [Integer] Terminal width
|
|
147
161
|
# @return [Integer] Number of lines
|
|
148
162
|
def line_count(width = TTY::Screen.width)
|
|
163
|
+
# Use effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
|
|
164
|
+
content_width = effective_content_width(width)
|
|
149
165
|
prompt_width = calculate_display_width(strip_ansi_codes(@prompt))
|
|
150
|
-
available_width =
|
|
166
|
+
available_width = content_width - prompt_width
|
|
151
167
|
return 1 if available_width <= 0
|
|
152
168
|
|
|
153
169
|
segments = wrap_line(@line, available_width)
|
|
@@ -200,6 +216,11 @@ module Clacky
|
|
|
200
216
|
|
|
201
217
|
{ action: :toggle_mode }
|
|
202
218
|
end
|
|
219
|
+
|
|
220
|
+
private def handle_toggle_expand
|
|
221
|
+
# Toggle expansion of diff display
|
|
222
|
+
{ action: :toggle_expand }
|
|
223
|
+
end
|
|
203
224
|
end
|
|
204
225
|
end
|
|
205
226
|
end
|
|
@@ -101,6 +101,8 @@ module Clacky
|
|
|
101
101
|
height += @images.size
|
|
102
102
|
|
|
103
103
|
# Calculate height considering wrapped lines
|
|
104
|
+
# Use effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
|
|
105
|
+
content_width = effective_content_width(@width)
|
|
104
106
|
@lines.each_with_index do |line, idx|
|
|
105
107
|
prefix = if idx == 0
|
|
106
108
|
prompt
|
|
@@ -108,7 +110,7 @@ module Clacky
|
|
|
108
110
|
" " * prompt.length
|
|
109
111
|
end
|
|
110
112
|
prefix_width = calculate_display_width(strip_ansi_codes(prefix))
|
|
111
|
-
available_width = [
|
|
113
|
+
available_width = [content_width - prefix_width, 20].max # At least 20 chars
|
|
112
114
|
wrapped_segments = wrap_line(line, available_width)
|
|
113
115
|
height += wrapped_segments.size
|
|
114
116
|
end
|
|
@@ -169,7 +171,7 @@ module Clacky
|
|
|
169
171
|
@command_suggestions.select_next
|
|
170
172
|
return { action: nil }
|
|
171
173
|
when :enter
|
|
172
|
-
# Accept selected command
|
|
174
|
+
# Accept selected command and submit immediately
|
|
173
175
|
if @command_suggestions.has_suggestions?
|
|
174
176
|
selected = @command_suggestions.selected_command_text
|
|
175
177
|
if selected
|
|
@@ -178,7 +180,8 @@ module Clacky
|
|
|
178
180
|
@line_index = 0
|
|
179
181
|
@cursor_position = selected.length
|
|
180
182
|
@command_suggestions.hide
|
|
181
|
-
|
|
183
|
+
# Submit the command immediately
|
|
184
|
+
return handle_enter
|
|
182
185
|
end
|
|
183
186
|
end
|
|
184
187
|
# Fall through to normal enter handling if no suggestion
|
|
@@ -186,20 +189,30 @@ module Clacky
|
|
|
186
189
|
@command_suggestions.hide
|
|
187
190
|
return { action: nil }
|
|
188
191
|
when :tab
|
|
189
|
-
# Tab
|
|
192
|
+
# Tab accepts the currently highlighted suggestion
|
|
190
193
|
if @command_suggestions.has_suggestions?
|
|
191
194
|
selected = @command_suggestions.selected_command_text
|
|
192
195
|
if selected
|
|
193
|
-
|
|
196
|
+
hint = @command_suggestions.selected_argument_hint
|
|
197
|
+
completed = "#{selected} "
|
|
198
|
+
@lines = [completed]
|
|
194
199
|
@line_index = 0
|
|
195
|
-
@cursor_position =
|
|
200
|
+
@cursor_position = completed.length
|
|
196
201
|
@command_suggestions.hide
|
|
202
|
+
# Show argument hint as a tip if available
|
|
203
|
+
set_tips("Usage: #{selected} #{hint}", type: :info) if hint && !hint.empty?
|
|
197
204
|
return { action: nil }
|
|
198
205
|
end
|
|
199
206
|
end
|
|
200
207
|
end
|
|
201
208
|
end
|
|
202
209
|
|
|
210
|
+
# Tab with no visible suggestions: trigger slash-command completion
|
|
211
|
+
if key == :tab
|
|
212
|
+
trigger_tab_completion
|
|
213
|
+
return { action: nil }
|
|
214
|
+
end
|
|
215
|
+
|
|
203
216
|
result = case key
|
|
204
217
|
when Hash
|
|
205
218
|
if key[:type] == :rapid_input
|
|
@@ -230,12 +243,16 @@ module Clacky
|
|
|
230
243
|
when :ctrl_c then handle_ctrl_c
|
|
231
244
|
when :ctrl_d then handle_ctrl_d
|
|
232
245
|
when :ctrl_v then handle_paste
|
|
246
|
+
when :ctrl_o then { action: :toggle_expand }
|
|
233
247
|
when :shift_tab then { action: :toggle_mode }
|
|
234
248
|
when :escape
|
|
235
249
|
if @command_suggestions.visible
|
|
236
250
|
@command_suggestions.hide
|
|
251
|
+
{ action: nil }
|
|
252
|
+
else
|
|
253
|
+
# Trigger time machine when ESC is pressed and suggestions not visible
|
|
254
|
+
{ action: :time_machine }
|
|
237
255
|
end
|
|
238
|
-
{ action: nil }
|
|
239
256
|
else
|
|
240
257
|
if key.is_a?(String) && key.length >= 1 && key.ord >= 32
|
|
241
258
|
insert_char(key)
|
|
@@ -318,6 +335,8 @@ module Clacky
|
|
|
318
335
|
def position_cursor(start_row)
|
|
319
336
|
# Calculate which wrapped line the cursor is on
|
|
320
337
|
cursor_row = start_row + 2 + @images.size # session_bar + separator + images
|
|
338
|
+
# Use effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
|
|
339
|
+
content_width = effective_content_width(@width)
|
|
321
340
|
|
|
322
341
|
# Add rows for lines before current line
|
|
323
342
|
@lines[0...@line_index].each_with_index do |line, idx|
|
|
@@ -327,7 +346,7 @@ module Clacky
|
|
|
327
346
|
" " * prompt.length
|
|
328
347
|
end
|
|
329
348
|
prefix_width = calculate_display_width(strip_ansi_codes(prefix))
|
|
330
|
-
available_width = [
|
|
349
|
+
available_width = [content_width - prefix_width, 20].max
|
|
331
350
|
wrapped_segments = wrap_line(line, available_width)
|
|
332
351
|
cursor_row += wrapped_segments.size
|
|
333
352
|
end
|
|
@@ -340,7 +359,7 @@ module Clacky
|
|
|
340
359
|
" " * prompt.length
|
|
341
360
|
end
|
|
342
361
|
prefix_width = calculate_display_width(strip_ansi_codes(prefix))
|
|
343
|
-
available_width = [
|
|
362
|
+
available_width = [content_width - prefix_width, 20].max
|
|
344
363
|
wrapped_segments = wrap_line(current, available_width)
|
|
345
364
|
|
|
346
365
|
# Find cursor segment and position within segment
|
|
@@ -610,9 +629,9 @@ module Clacky
|
|
|
610
629
|
# Shows suggestions when input starts with /
|
|
611
630
|
private def update_command_suggestions
|
|
612
631
|
return unless @command_suggestions
|
|
613
|
-
|
|
632
|
+
|
|
614
633
|
current = current_line.strip
|
|
615
|
-
|
|
634
|
+
|
|
616
635
|
# Check if we should show suggestions (input starts with /)
|
|
617
636
|
if current.start_with?('/') && @line_index == 0
|
|
618
637
|
# Extract the filter text (everything after /)
|
|
@@ -623,16 +642,37 @@ module Clacky
|
|
|
623
642
|
end
|
|
624
643
|
end
|
|
625
644
|
|
|
645
|
+
# Trigger tab completion: show all commands or filter by current slash input
|
|
646
|
+
# Called when Tab is pressed and no suggestions dropdown is visible
|
|
647
|
+
private def trigger_tab_completion
|
|
648
|
+
return unless @command_suggestions
|
|
649
|
+
|
|
650
|
+
current = current_line.strip
|
|
651
|
+
|
|
652
|
+
if current.empty?
|
|
653
|
+
# Empty input: type "/" and show all commands
|
|
654
|
+
insert_char("/")
|
|
655
|
+
@command_suggestions.show("")
|
|
656
|
+
elsif current.start_with?("/")
|
|
657
|
+
# Already typing a slash command: show/refresh filtered suggestions
|
|
658
|
+
filter_text = current[1..-1] || ""
|
|
659
|
+
@command_suggestions.show(filter_text)
|
|
660
|
+
end
|
|
661
|
+
# Tab on normal text has no effect
|
|
662
|
+
end
|
|
663
|
+
|
|
626
664
|
# Render all input lines with auto-wrap support
|
|
627
665
|
# @param start_row [Integer] Starting row position
|
|
628
666
|
# @return [Integer] Next available row after rendering all lines
|
|
629
667
|
def render_input_lines(start_row)
|
|
630
668
|
current_row = start_row
|
|
669
|
+
# Use effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
|
|
670
|
+
content_width = effective_content_width(@width)
|
|
631
671
|
|
|
632
672
|
@lines.each_with_index do |line, line_idx|
|
|
633
673
|
prefix = calculate_line_prefix(line_idx)
|
|
634
674
|
prefix_width = calculate_display_width(strip_ansi_codes(prefix))
|
|
635
|
-
available_width =
|
|
675
|
+
available_width = content_width - prefix_width
|
|
636
676
|
wrapped_segments = wrap_line(line, available_width)
|
|
637
677
|
|
|
638
678
|
wrapped_segments.each_with_index do |segment_info, wrap_idx|
|
|
@@ -748,6 +788,11 @@ module Clacky
|
|
|
748
788
|
def handle_enter
|
|
749
789
|
text = current_value.strip
|
|
750
790
|
|
|
791
|
+
# Prepare display content and data BEFORE clearing
|
|
792
|
+
content_to_display = current_content
|
|
793
|
+
result_text = current_value
|
|
794
|
+
result_images = @images.dup
|
|
795
|
+
|
|
751
796
|
# Handle commands (with or without slash)
|
|
752
797
|
if text.start_with?('/')
|
|
753
798
|
# Check if it's a command (single slash followed by English letters only)
|
|
@@ -755,10 +800,13 @@ module Clacky
|
|
|
755
800
|
if text =~ /^\/([a-zA-Z-]+)$/
|
|
756
801
|
case text
|
|
757
802
|
when '/clear'
|
|
803
|
+
add_to_history(result_text) unless result_text.empty?
|
|
758
804
|
clear
|
|
759
|
-
return { action: :clear_output }
|
|
805
|
+
return { action: :clear_output, data: { text: result_text, images: result_images, display: content_to_display } }
|
|
760
806
|
when '/help'
|
|
761
|
-
|
|
807
|
+
add_to_history(result_text) unless result_text.empty?
|
|
808
|
+
clear
|
|
809
|
+
return { action: :help, data: { text: result_text, images: result_images, display: content_to_display } }
|
|
762
810
|
when '/exit', '/quit'
|
|
763
811
|
return { action: :exit }
|
|
764
812
|
else
|
|
@@ -768,7 +816,9 @@ module Clacky
|
|
|
768
816
|
end
|
|
769
817
|
# If it's not a command pattern (e.g., /xxx/xxxx), treat as normal input
|
|
770
818
|
elsif text == '?'
|
|
771
|
-
|
|
819
|
+
add_to_history(result_text) unless result_text.empty?
|
|
820
|
+
clear
|
|
821
|
+
return { action: :help, data: { text: result_text, images: result_images, display: content_to_display } }
|
|
772
822
|
elsif text == 'exit' || text == 'quit'
|
|
773
823
|
return { action: :exit }
|
|
774
824
|
end
|
|
@@ -777,10 +827,6 @@ module Clacky
|
|
|
777
827
|
return { action: nil }
|
|
778
828
|
end
|
|
779
829
|
|
|
780
|
-
content_to_display = current_content
|
|
781
|
-
result_text = current_value
|
|
782
|
-
result_images = @images.dup
|
|
783
|
-
|
|
784
830
|
add_to_history(result_text) unless result_text.empty?
|
|
785
831
|
clear
|
|
786
832
|
|
|
@@ -1168,8 +1214,6 @@ module Clacky
|
|
|
1168
1214
|
:magenta
|
|
1169
1215
|
when /confirm_safes/
|
|
1170
1216
|
:cyan
|
|
1171
|
-
when /confirm_edits/
|
|
1172
|
-
:green
|
|
1173
1217
|
when /plan_only/
|
|
1174
1218
|
:blue
|
|
1175
1219
|
else
|