openclacky 0.8.2 → 0.8.4
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 +34 -0
- data/lib/clacky/agent/session_serializer.rb +31 -0
- data/lib/clacky/agent/skill_manager.rb +59 -0
- data/lib/clacky/agent.rb +7 -2
- data/lib/clacky/agent_config.rb +10 -0
- data/lib/clacky/brand_config.rb +111 -24
- data/lib/clacky/cli.rb +1 -1
- data/lib/clacky/default_skills/onboard/SKILL.md +2 -2
- data/lib/clacky/server/http_server.rb +7 -4
- data/lib/clacky/skill_loader.rb +22 -18
- data/lib/clacky/ui2/layout_manager.rb +5 -0
- data/lib/clacky/ui2/screen_buffer.rb +24 -7
- data/lib/clacky/ui2/ui_controller.rb +56 -19
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +947 -337
- data/lib/clacky/web/app.js +30 -11
- data/lib/clacky/web/index.html +108 -30
- data/lib/clacky/web/onboard.js +92 -16
- data/lib/clacky/web/sessions.js +78 -3
- data/lib/clacky/web/settings.js +179 -26
- data/lib/clacky/web/skills.js +7 -3
- data/lib/clacky/web/tasks.js +34 -8
- data/lib/clacky/web/theme.js +67 -0
- metadata +16 -1
|
@@ -15,6 +15,10 @@ module Clacky
|
|
|
15
15
|
@buffer = []
|
|
16
16
|
@last_input_time = nil
|
|
17
17
|
@rapid_input_threshold = 0.01 # 10ms threshold for detecting paste-like rapid input
|
|
18
|
+
|
|
19
|
+
# Keep stdin in UTF-8 mode so getc returns complete multi-byte characters (e.g. CJK).
|
|
20
|
+
# Switching to BINARY would cause getc to return one byte at a time, breaking Chinese input.
|
|
21
|
+
$stdin.set_encoding('UTF-8')
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
# Move cursor to specific position (0-indexed)
|
|
@@ -138,8 +142,6 @@ module Clacky
|
|
|
138
142
|
# @param timeout [Float] Timeout in seconds
|
|
139
143
|
# @return [Symbol, String, Hash, nil] Key symbol, character, or { type: :rapid_input, text: String }
|
|
140
144
|
def read_key(timeout: nil)
|
|
141
|
-
$stdin.set_encoding('UTF-8')
|
|
142
|
-
|
|
143
145
|
current_time = Time.now.to_f
|
|
144
146
|
is_rapid_input = @last_input_time && (current_time - @last_input_time) < @rapid_input_threshold
|
|
145
147
|
@last_input_time = current_time
|
|
@@ -147,8 +149,9 @@ module Clacky
|
|
|
147
149
|
char = read_char(timeout: timeout)
|
|
148
150
|
return nil unless char
|
|
149
151
|
|
|
150
|
-
#
|
|
151
|
-
|
|
152
|
+
# Convert raw BINARY bytes to valid UTF-8. Invalid/undefined bytes are dropped
|
|
153
|
+
# rather than raising ArgumentError (which would crash the input loop).
|
|
154
|
+
char = safe_to_utf8(char) if char.is_a?(String)
|
|
152
155
|
|
|
153
156
|
# Handle escape sequences for special keys
|
|
154
157
|
if char == "\e"
|
|
@@ -179,7 +182,6 @@ module Clacky
|
|
|
179
182
|
# If this is rapid input or there are more characters available
|
|
180
183
|
if is_rapid_input || has_more_input
|
|
181
184
|
buffer = char.to_s.dup
|
|
182
|
-
buffer.force_encoding('UTF-8')
|
|
183
185
|
|
|
184
186
|
# Keep reading available characters
|
|
185
187
|
loop_count = 0
|
|
@@ -190,10 +192,10 @@ module Clacky
|
|
|
190
192
|
has_data = IO.select([$stdin], nil, nil, 0)
|
|
191
193
|
|
|
192
194
|
if has_data
|
|
193
|
-
next_char = $stdin.getc
|
|
195
|
+
next_char = $stdin.getc rescue nil
|
|
194
196
|
break unless next_char
|
|
195
197
|
|
|
196
|
-
next_char = next_char
|
|
198
|
+
next_char = safe_to_utf8(next_char)
|
|
197
199
|
buffer << next_char
|
|
198
200
|
loop_count += 1
|
|
199
201
|
empty_checks = 0 # Reset empty check counter
|
|
@@ -213,6 +215,8 @@ module Clacky
|
|
|
213
215
|
|
|
214
216
|
# If we buffered multiple characters or newlines, treat as rapid input (paste)
|
|
215
217
|
if buffer.length > 1 || buffer.include?("\n") || buffer.include?("\r")
|
|
218
|
+
# Ensure the accumulated buffer is valid UTF-8 before regex operations
|
|
219
|
+
buffer = safe_to_utf8(buffer)
|
|
216
220
|
# Remove any trailing \r or \n from rapid input buffer
|
|
217
221
|
cleaned_buffer = buffer.gsub(/[\r\n]+\z/, '')
|
|
218
222
|
return { type: :rapid_input, text: cleaned_buffer } if cleaned_buffer.length > 0
|
|
@@ -251,6 +255,19 @@ module Clacky
|
|
|
251
255
|
end
|
|
252
256
|
|
|
253
257
|
private
|
|
258
|
+
|
|
259
|
+
# Ensure a string is valid UTF-8.
|
|
260
|
+
# stdin stays in UTF-8 mode so getc returns complete characters (including CJK).
|
|
261
|
+
# This method handles the rare case where an invalid byte slips through
|
|
262
|
+
# (e.g. a stray terminal escape or a partial sequence) by scrubbing it out
|
|
263
|
+
# rather than letting ArgumentError crash the input loop.
|
|
264
|
+
# @param str [String] String from getc (UTF-8 encoded, but may have invalid bytes)
|
|
265
|
+
# @return [String] Valid UTF-8 string
|
|
266
|
+
private def safe_to_utf8(str)
|
|
267
|
+
return str if str.valid_encoding?
|
|
268
|
+
|
|
269
|
+
str.encode('UTF-8', 'UTF-8', invalid: :replace, undef: :replace, replace: '')
|
|
270
|
+
end
|
|
254
271
|
end
|
|
255
272
|
end
|
|
256
273
|
end
|
|
@@ -90,6 +90,13 @@ module Clacky
|
|
|
90
90
|
input_loop
|
|
91
91
|
end
|
|
92
92
|
|
|
93
|
+
# Set skill loader for command suggestions in the input area
|
|
94
|
+
# @param skill_loader [Clacky::SkillLoader] The skill loader instance
|
|
95
|
+
# @param agent_profile [Clacky::AgentProfile, nil] Current agent profile for skill filtering
|
|
96
|
+
def set_skill_loader(skill_loader, agent_profile = nil)
|
|
97
|
+
@input_area.set_skill_loader(skill_loader, agent_profile)
|
|
98
|
+
end
|
|
99
|
+
|
|
93
100
|
# Update session bar with current stats
|
|
94
101
|
# @param tasks [Integer] Number of completed tasks (optional)
|
|
95
102
|
# @param cost [Float] Total cost (optional)
|
|
@@ -170,11 +177,11 @@ module Clacky
|
|
|
170
177
|
@time_machine_callback = block
|
|
171
178
|
end
|
|
172
179
|
|
|
173
|
-
# Set
|
|
174
|
-
# @param
|
|
180
|
+
# Set agent for command suggestions
|
|
181
|
+
# @param agent [Clacky::Agent] The agent instance with skill management
|
|
175
182
|
# @param agent_profile [Clacky::AgentProfile, nil] Current agent profile for skill filtering
|
|
176
|
-
def
|
|
177
|
-
@input_area.
|
|
183
|
+
def set_agent(agent, agent_profile = nil)
|
|
184
|
+
@input_area.set_agent(agent, agent_profile)
|
|
178
185
|
end
|
|
179
186
|
|
|
180
187
|
# Append output to the output area
|
|
@@ -450,6 +457,10 @@ module Clacky
|
|
|
450
457
|
@progress_message = message || Clacky::THINKING_VERBS.sample
|
|
451
458
|
@progress_start_time = Time.now
|
|
452
459
|
@progress_output_buffer = output_buffer
|
|
460
|
+
# Flag used by the progress thread to know when to stop gracefully.
|
|
461
|
+
# Using a flag + join is safe because Thread#kill can interrupt a thread
|
|
462
|
+
# while it holds @render_mutex, causing a permanent deadlock.
|
|
463
|
+
@progress_thread_stop = false
|
|
453
464
|
|
|
454
465
|
# Show initial progress (yellow, active)
|
|
455
466
|
append_output("") if prefix_newline
|
|
@@ -459,15 +470,19 @@ module Clacky
|
|
|
459
470
|
|
|
460
471
|
# Start background thread to update elapsed time
|
|
461
472
|
@progress_thread = Thread.new do
|
|
462
|
-
|
|
473
|
+
until @progress_thread_stop
|
|
463
474
|
sleep 0.5
|
|
464
|
-
next
|
|
475
|
+
next if @progress_thread_stop
|
|
476
|
+
|
|
477
|
+
start = @progress_start_time
|
|
478
|
+
next unless start
|
|
465
479
|
|
|
466
|
-
elapsed = (Time.now -
|
|
467
|
-
|
|
480
|
+
elapsed = (Time.now - start).to_i
|
|
481
|
+
buf = @progress_output_buffer
|
|
482
|
+
hint = buf ? "(Ctrl+C to interrupt · Ctrl+O to view output · #{elapsed}s)" : "(Ctrl+C to interrupt · #{elapsed}s)"
|
|
468
483
|
update_progress_line(@renderer.render_working("#{@progress_message}… #{hint}"))
|
|
469
484
|
end
|
|
470
|
-
rescue
|
|
485
|
+
rescue StandardError
|
|
471
486
|
# Silently handle thread errors
|
|
472
487
|
end
|
|
473
488
|
end
|
|
@@ -489,14 +504,32 @@ module Clacky
|
|
|
489
504
|
end
|
|
490
505
|
end
|
|
491
506
|
|
|
492
|
-
# Stop the
|
|
507
|
+
# Stop the fullscreen refresh thread gracefully via flag + join.
|
|
508
|
+
def stop_fullscreen_refresh_thread
|
|
509
|
+
@fullscreen_refresh_stop = true
|
|
510
|
+
if @fullscreen_refresh_thread&.alive?
|
|
511
|
+
joined = @fullscreen_refresh_thread.join(1.0)
|
|
512
|
+
@fullscreen_refresh_thread.kill unless joined
|
|
513
|
+
end
|
|
514
|
+
@fullscreen_refresh_thread = nil
|
|
515
|
+
@fullscreen_refresh_stop = false
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# Stop the progress update thread gracefully.
|
|
519
|
+
# We signal the thread via a stop flag and then join it, avoiding Thread#kill
|
|
520
|
+
# which can interrupt a thread mid-critical-section (e.g. while holding
|
|
521
|
+
# @render_mutex) and leave the mutex permanently locked.
|
|
493
522
|
def stop_progress_thread
|
|
494
523
|
@progress_start_time = nil
|
|
495
|
-
@progress_output_buffer = nil
|
|
524
|
+
@progress_output_buffer = nil
|
|
525
|
+
@progress_thread_stop = true
|
|
496
526
|
if @progress_thread&.alive?
|
|
497
|
-
|
|
498
|
-
|
|
527
|
+
# Join with a short timeout; fall back to kill only as a last resort
|
|
528
|
+
joined = @progress_thread.join(1.0)
|
|
529
|
+
@progress_thread.kill unless joined
|
|
499
530
|
end
|
|
531
|
+
@progress_thread = nil
|
|
532
|
+
@progress_thread_stop = false
|
|
500
533
|
end
|
|
501
534
|
|
|
502
535
|
# Show info message
|
|
@@ -703,12 +736,15 @@ module Clacky
|
|
|
703
736
|
lines = build_command_output_lines
|
|
704
737
|
@layout.enter_fullscreen(lines, hint: "Press Ctrl+O to return · Output updates in real-time")
|
|
705
738
|
|
|
706
|
-
# Start background thread to refresh fullscreen content in real-time
|
|
739
|
+
# Start background thread to refresh fullscreen content in real-time.
|
|
740
|
+
# Use a dedicated stop flag so we can join() the thread cleanly and
|
|
741
|
+
# avoid Thread#kill interrupting the thread while it holds @render_mutex.
|
|
707
742
|
buffer_ref = @progress_output_buffer
|
|
743
|
+
@fullscreen_refresh_stop = false
|
|
708
744
|
@fullscreen_refresh_thread = Thread.new do
|
|
709
|
-
|
|
745
|
+
until @fullscreen_refresh_stop || !@layout.fullscreen_mode?
|
|
710
746
|
sleep 0.3
|
|
711
|
-
|
|
747
|
+
next if @fullscreen_refresh_stop || !@layout.fullscreen_mode?
|
|
712
748
|
|
|
713
749
|
updated_lines = build_command_output_lines_from(buffer_ref)
|
|
714
750
|
@layout.refresh_fullscreen(updated_lines)
|
|
@@ -863,9 +899,10 @@ module Clacky
|
|
|
863
899
|
# If in fullscreen mode, only handle Ctrl+O to exit
|
|
864
900
|
if @layout.fullscreen_mode?
|
|
865
901
|
if key == :ctrl_o
|
|
866
|
-
#
|
|
867
|
-
|
|
868
|
-
@
|
|
902
|
+
# Signal the real-time refresh thread to stop gracefully, then join it.
|
|
903
|
+
# Avoid Thread#kill which can interrupt the thread mid-render and
|
|
904
|
+
# leave @render_mutex permanently locked.
|
|
905
|
+
stop_fullscreen_refresh_thread
|
|
869
906
|
@layout.exit_fullscreen
|
|
870
907
|
# Restore main screen content after returning from alternate buffer
|
|
871
908
|
@layout.rerender_all
|
data/lib/clacky/version.rb
CHANGED