openclacky 1.3.4 → 1.3.6
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 +39 -0
- data/lib/clacky/agent/fake_tool_call_detector.rb +52 -0
- data/lib/clacky/agent/message_compressor.rb +32 -8
- data/lib/clacky/agent/message_compressor_helper.rb +113 -12
- data/lib/clacky/agent/session_serializer.rb +3 -2
- data/lib/clacky/agent/tool_executor.rb +0 -12
- data/lib/clacky/agent.rb +74 -12
- data/lib/clacky/api_extension.rb +81 -0
- data/lib/clacky/api_extension_loader.rb +13 -1
- data/lib/clacky/cli.rb +0 -1
- data/lib/clacky/client.rb +14 -17
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +22 -0
- data/lib/clacky/default_agents/base_prompt.md +1 -0
- data/lib/clacky/default_extensions/meeting/handler.rb +331 -0
- data/lib/clacky/default_extensions/meeting/meeting.js +790 -0
- data/lib/clacky/default_extensions/meeting/meta.yml +3 -0
- data/lib/clacky/default_extensions/meeting/skills/meeting-summarizer/SKILL.md +44 -0
- data/lib/clacky/default_skills/media-gen/SKILL.md +63 -0
- data/lib/clacky/default_skills/media-gen/scripts/video_seq.sh +114 -0
- data/lib/clacky/json_ui_controller.rb +1 -1
- data/lib/clacky/media/base.rb +60 -0
- data/lib/clacky/media/dashscope.rb +385 -21
- data/lib/clacky/media/gemini.rb +9 -0
- data/lib/clacky/media/generator.rb +52 -0
- data/lib/clacky/media/openai_compat.rb +166 -0
- data/lib/clacky/null_ui_controller.rb +13 -0
- data/lib/clacky/plain_ui_controller.rb +1 -1
- data/lib/clacky/providers.rb +50 -2
- data/lib/clacky/rich_ui/rich_ui_controller.rb +1 -1
- data/lib/clacky/server/channel/channel_ui_controller.rb +1 -1
- data/lib/clacky/server/http_server.rb +260 -122
- data/lib/clacky/server/session_registry.rb +54 -3
- data/lib/clacky/server/web_ui_controller.rb +3 -2
- data/lib/clacky/session_manager.rb +35 -4
- data/lib/clacky/skill_loader.rb +14 -2
- data/lib/clacky/tools/terminal/output_cleaner.rb +1 -3
- data/lib/clacky/tools/terminal.rb +0 -43
- data/lib/clacky/ui2/components/modal_component.rb +1 -1
- data/lib/clacky/ui2/layout_manager.rb +0 -5
- data/lib/clacky/ui2/progress_handle.rb +0 -3
- data/lib/clacky/ui2/ui_controller.rb +140 -40
- data/lib/clacky/ui_interface.rb +10 -1
- data/lib/clacky/utils/encoding.rb +25 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +199 -23
- data/lib/clacky/web/components/onboard.js +1 -14
- data/lib/clacky/web/components/sidebar.js +1 -3
- data/lib/clacky/web/features/backup/store.js +23 -0
- data/lib/clacky/web/features/backup/view.js +49 -22
- data/lib/clacky/web/features/brand/view.js +8 -5
- data/lib/clacky/web/features/channels/store.js +1 -20
- data/lib/clacky/web/features/mcp/store.js +1 -20
- data/lib/clacky/web/features/profile/store.js +1 -13
- data/lib/clacky/web/features/profile/view.js +16 -4
- data/lib/clacky/web/features/skills/store.js +6 -21
- data/lib/clacky/web/features/tasks/view.js +77 -28
- data/lib/clacky/web/features/version/store.js +2 -0
- data/lib/clacky/web/i18n.js +46 -3
- data/lib/clacky/web/index.html +67 -26
- data/lib/clacky/web/sessions.js +177 -87
- data/lib/clacky/web/settings.js +34 -2
- data/lib/clacky/web/ws-dispatcher.js +12 -4
- data/lib/clacky.rb +12 -5
- metadata +8 -1
|
@@ -17,7 +17,7 @@ module Clacky
|
|
|
17
17
|
include Clacky::UIInterface
|
|
18
18
|
|
|
19
19
|
attr_reader :layout, :renderer, :running, :inline_input, :input_area
|
|
20
|
-
attr_accessor :config
|
|
20
|
+
attr_accessor :config, :available_models
|
|
21
21
|
|
|
22
22
|
def initialize(config = {})
|
|
23
23
|
@renderer = ViewRenderer.new
|
|
@@ -38,6 +38,7 @@ module Clacky
|
|
|
38
38
|
@todo_area = Components::TodoArea.new
|
|
39
39
|
@welcome_banner = Components::WelcomeBanner.new
|
|
40
40
|
@inline_input = nil # Created when needed
|
|
41
|
+
@feedback_countdown = nil # Active auto-approve feedback countdown session
|
|
41
42
|
@layout = LayoutManager.new(
|
|
42
43
|
input_area: @input_area,
|
|
43
44
|
todo_area: @todo_area
|
|
@@ -663,22 +664,13 @@ module Clacky
|
|
|
663
664
|
|
|
664
665
|
# Called by ProgressHandle#finish.
|
|
665
666
|
def unregister_progress(handle, final_frame:)
|
|
666
|
-
Clacky::Logger.warn("[ph_debug] unreg_entry", oid: handle.object_id, eid: handle.entry_id, top: @progress_stack.last == handle, stack_size: @progress_stack.size, ff: final_frame.to_s[0, 200])
|
|
667
667
|
@progress_mutex.synchronize do
|
|
668
|
-
# If this handle still holds its entry (it's currently top), we
|
|
669
|
-
# render one last frame there and release the id. If it was
|
|
670
|
-
# previously detached (someone above is still active), its entry
|
|
671
|
-
# is already gone and the final_frame is simply dropped.
|
|
672
668
|
if handle.entry_id
|
|
673
669
|
if final_frame && !final_frame.to_s.strip.empty?
|
|
674
|
-
Clacky::Logger.warn("[ph_debug] unreg_update_entry", oid: handle.object_id, eid: handle.entry_id)
|
|
675
670
|
update_entry(handle.entry_id, @renderer.render_progress(final_frame))
|
|
676
671
|
else
|
|
677
|
-
Clacky::Logger.warn("[ph_debug] unreg_remove_entry", oid: handle.object_id, eid: handle.entry_id)
|
|
678
672
|
remove_entry(handle.entry_id)
|
|
679
673
|
end
|
|
680
|
-
else
|
|
681
|
-
Clacky::Logger.warn("[ph_debug] unreg_no_entry_id", oid: handle.object_id)
|
|
682
674
|
end
|
|
683
675
|
|
|
684
676
|
@progress_stack.delete(handle)
|
|
@@ -874,7 +866,7 @@ module Clacky
|
|
|
874
866
|
|
|
875
867
|
# Show error message
|
|
876
868
|
# @param message [String] Error message
|
|
877
|
-
def show_error(message, code: nil, top_up_url: nil)
|
|
869
|
+
def show_error(message, code: nil, top_up_url: nil, raw_message: nil)
|
|
878
870
|
output = @renderer.render_error(message)
|
|
879
871
|
append_output(output)
|
|
880
872
|
end
|
|
@@ -1054,6 +1046,99 @@ module Clacky
|
|
|
1054
1046
|
end
|
|
1055
1047
|
end
|
|
1056
1048
|
|
|
1049
|
+
# Auto-approve countdown for request_user_feedback: show a single live
|
|
1050
|
+
# countdown line. If the user presses any key before timeout, collect
|
|
1051
|
+
# their answer and return it (intervention). Otherwise return :timeout so
|
|
1052
|
+
# the agent auto-decides and continues.
|
|
1053
|
+
# @param seconds [Integer] Countdown duration
|
|
1054
|
+
# @return [String, Symbol] feedback string, "" (bare Enter), or :timeout
|
|
1055
|
+
# Show a live single-line countdown in the output area while keeping the
|
|
1056
|
+
# normal input box usable. The agent thread blocks here until either:
|
|
1057
|
+
# * the user submits a message in the regular input box -> returns the text
|
|
1058
|
+
# * the user starts typing (cancels the auto-timeout but keeps waiting)
|
|
1059
|
+
# * the countdown reaches zero with no interaction -> returns :timeout
|
|
1060
|
+
# @param seconds [Integer] Countdown duration
|
|
1061
|
+
# @return [String, Symbol] feedback text, or :timeout
|
|
1062
|
+
def request_feedback_with_countdown(seconds: 10)
|
|
1063
|
+
theme = ThemeManager.current_theme
|
|
1064
|
+
|
|
1065
|
+
queue = Queue.new
|
|
1066
|
+
entry_id = @layout.append_output(countdown_prompt(seconds, theme))
|
|
1067
|
+
|
|
1068
|
+
session = {
|
|
1069
|
+
queue: queue,
|
|
1070
|
+
entry_id: entry_id,
|
|
1071
|
+
intervened: false,
|
|
1072
|
+
watchdog: nil
|
|
1073
|
+
}
|
|
1074
|
+
@feedback_countdown = session
|
|
1075
|
+
|
|
1076
|
+
session[:watchdog] = Thread.new do
|
|
1077
|
+
remaining = seconds.to_i
|
|
1078
|
+
while remaining.positive?
|
|
1079
|
+
break if session[:intervened]
|
|
1080
|
+
|
|
1081
|
+
@layout.replace_entry(entry_id, countdown_prompt(remaining, theme))
|
|
1082
|
+
sleep 1
|
|
1083
|
+
remaining -= 1
|
|
1084
|
+
end
|
|
1085
|
+
queue.push(:timeout) unless session[:intervened]
|
|
1086
|
+
end
|
|
1087
|
+
|
|
1088
|
+
result = queue.pop
|
|
1089
|
+
session[:watchdog].kill if session[:watchdog]&.alive?
|
|
1090
|
+
@feedback_countdown = nil
|
|
1091
|
+
@layout.remove_entry(entry_id) if entry_id
|
|
1092
|
+
@layout.recalculate_layout
|
|
1093
|
+
@layout.render_all
|
|
1094
|
+
|
|
1095
|
+
if result == :timeout
|
|
1096
|
+
append_output(theme.format_text(" No response — continuing automatically.", :thinking))
|
|
1097
|
+
elsif result.to_s.strip.empty?
|
|
1098
|
+
append_output(theme.format_text(" → (continue)", :success))
|
|
1099
|
+
result = ""
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
result
|
|
1103
|
+
end
|
|
1104
|
+
|
|
1105
|
+
# Cancel the countdown's auto-timeout when the user starts interacting,
|
|
1106
|
+
# but keep waiting for their submitted answer. Returns true if a
|
|
1107
|
+
# countdown was active.
|
|
1108
|
+
private def intervene_feedback_countdown
|
|
1109
|
+
session = @feedback_countdown
|
|
1110
|
+
return false unless session && !session[:intervened]
|
|
1111
|
+
|
|
1112
|
+
session[:intervened] = true
|
|
1113
|
+
session[:watchdog].kill if session[:watchdog]&.alive?
|
|
1114
|
+
if session[:entry_id]
|
|
1115
|
+
@layout.remove_entry(session[:entry_id])
|
|
1116
|
+
session[:entry_id] = nil
|
|
1117
|
+
@layout.recalculate_layout
|
|
1118
|
+
@layout.render_all
|
|
1119
|
+
end
|
|
1120
|
+
true
|
|
1121
|
+
end
|
|
1122
|
+
|
|
1123
|
+
private def countdown_prompt(remaining, theme)
|
|
1124
|
+
theme.format_text(" Auto-continuing in #{remaining}s — type your answer to step in…", :info)
|
|
1125
|
+
end
|
|
1126
|
+
|
|
1127
|
+
# Whether a keystroke should count as the user stepping into a countdown.
|
|
1128
|
+
# Plain typing and pastes do; pure scroll/navigation keys do not.
|
|
1129
|
+
private def countdown_intervening_key?(key)
|
|
1130
|
+
case key
|
|
1131
|
+
when Hash
|
|
1132
|
+
key[:type] == :rapid_input
|
|
1133
|
+
when String
|
|
1134
|
+
key.length >= 1 && key.ord >= 32
|
|
1135
|
+
when :backspace, :enter
|
|
1136
|
+
true
|
|
1137
|
+
else
|
|
1138
|
+
false
|
|
1139
|
+
end
|
|
1140
|
+
end
|
|
1141
|
+
|
|
1057
1142
|
# Show diff between old and new content
|
|
1058
1143
|
# @param old_content [String] Old content
|
|
1059
1144
|
# @param new_content [String] New content
|
|
@@ -1206,7 +1291,7 @@ module Clacky
|
|
|
1206
1291
|
# Check if API key is configured and show warning if missing
|
|
1207
1292
|
private def check_api_key_configuration
|
|
1208
1293
|
config = Clacky::AgentConfig.load
|
|
1209
|
-
|
|
1294
|
+
|
|
1210
1295
|
if !config.models_configured?
|
|
1211
1296
|
show_warning("No models configured! Please run /config to set up your models and API keys.")
|
|
1212
1297
|
elsif config.api_key.nil? || config.api_key.empty?
|
|
@@ -1250,7 +1335,7 @@ module Clacky
|
|
|
1250
1335
|
while @running
|
|
1251
1336
|
# Process any pending resize events
|
|
1252
1337
|
@layout.process_pending_resize
|
|
1253
|
-
|
|
1338
|
+
|
|
1254
1339
|
key = @layout.screen.read_key(timeout: 0.1)
|
|
1255
1340
|
next unless key
|
|
1256
1341
|
|
|
@@ -1291,6 +1376,13 @@ module Clacky
|
|
|
1291
1376
|
return
|
|
1292
1377
|
end
|
|
1293
1378
|
|
|
1379
|
+
# During an auto-approve feedback countdown the normal input box stays
|
|
1380
|
+
# live; the first meaningful keystroke cancels the auto-timeout so the
|
|
1381
|
+
# user can finish typing their answer without being rushed.
|
|
1382
|
+
if @feedback_countdown && !@feedback_countdown[:intervened] && countdown_intervening_key?(key)
|
|
1383
|
+
intervene_feedback_countdown
|
|
1384
|
+
end
|
|
1385
|
+
|
|
1294
1386
|
result = @input_area.handle_key(key)
|
|
1295
1387
|
|
|
1296
1388
|
# Handle height change first
|
|
@@ -1416,6 +1508,14 @@ module Clacky
|
|
|
1416
1508
|
append_output(output)
|
|
1417
1509
|
end
|
|
1418
1510
|
|
|
1511
|
+
# If an auto-approve feedback countdown is waiting for an answer, this
|
|
1512
|
+
# submission IS that answer — deliver it to the waiting agent thread
|
|
1513
|
+
# instead of starting a brand-new turn.
|
|
1514
|
+
if (session = @feedback_countdown)
|
|
1515
|
+
session[:queue].push(data[:text].to_s)
|
|
1516
|
+
return
|
|
1517
|
+
end
|
|
1518
|
+
|
|
1419
1519
|
# Then call callback (allows interrupting previous agent before processing new input)
|
|
1420
1520
|
@input_callback&.call(data[:text], data[:files])
|
|
1421
1521
|
end
|
|
@@ -1425,31 +1525,31 @@ module Clacky
|
|
|
1425
1525
|
# @return [Hash, nil] Hash with updated config values, or nil if cancelled
|
|
1426
1526
|
public def show_config_modal(current_config, test_callback: nil)
|
|
1427
1527
|
modal = Components::ModalComponent.new
|
|
1428
|
-
|
|
1528
|
+
|
|
1429
1529
|
loop do
|
|
1430
1530
|
# Build menu choices
|
|
1431
1531
|
choices = []
|
|
1432
|
-
|
|
1532
|
+
|
|
1433
1533
|
# Add model list
|
|
1434
1534
|
current_config.models.each_with_index do |model, idx|
|
|
1435
1535
|
is_current = (idx == current_config.current_model_index)
|
|
1436
1536
|
model_name = model["model"] || "unnamed"
|
|
1437
1537
|
masked_key = mask_api_key(model["api_key"])
|
|
1438
|
-
|
|
1538
|
+
|
|
1439
1539
|
# Add type badge if present
|
|
1440
1540
|
type_badge = case model["type"]
|
|
1441
1541
|
when "default" then "[default] "
|
|
1442
1542
|
when "lite" then "[lite] "
|
|
1443
1543
|
else ""
|
|
1444
1544
|
end
|
|
1445
|
-
|
|
1545
|
+
|
|
1446
1546
|
display_name = "#{type_badge}#{model_name} (#{masked_key})"
|
|
1447
1547
|
choices << {
|
|
1448
1548
|
name: display_name,
|
|
1449
1549
|
value: { action: :switch, model_id: model["id"] }
|
|
1450
1550
|
}
|
|
1451
1551
|
end
|
|
1452
|
-
|
|
1552
|
+
|
|
1453
1553
|
# Add action buttons
|
|
1454
1554
|
choices << { name: "─" * 50, disabled: true }
|
|
1455
1555
|
choices << { name: "[+] Add New Model", value: { action: :add } }
|
|
@@ -1458,16 +1558,16 @@ module Clacky
|
|
|
1458
1558
|
choices << { name: "[-] Delete Model", value: { action: :delete } } if current_config.models.length > 1
|
|
1459
1559
|
end
|
|
1460
1560
|
choices << { name: "[X] Close", value: { action: :close } }
|
|
1461
|
-
|
|
1561
|
+
|
|
1462
1562
|
# Show menu
|
|
1463
1563
|
result = modal.show(
|
|
1464
|
-
title: "Model Configuration",
|
|
1564
|
+
title: "Model Configuration",
|
|
1465
1565
|
choices: choices,
|
|
1466
1566
|
on_close: -> { @layout.rerender_all }
|
|
1467
1567
|
)
|
|
1468
|
-
|
|
1568
|
+
|
|
1469
1569
|
return nil if result.nil?
|
|
1470
|
-
|
|
1570
|
+
|
|
1471
1571
|
case result[:action]
|
|
1472
1572
|
when :switch
|
|
1473
1573
|
# Just signal the caller which model to switch to.
|
|
@@ -1699,16 +1799,16 @@ module Clacky
|
|
|
1699
1799
|
|
|
1700
1800
|
result # Return selected task_id or nil
|
|
1701
1801
|
end
|
|
1702
|
-
|
|
1802
|
+
|
|
1703
1803
|
# Show form for editing a model
|
|
1704
1804
|
# @param model [Hash, nil] Existing model hash or nil for new model
|
|
1705
1805
|
# @return [Hash, nil] Updated model hash or nil if cancelled
|
|
1706
1806
|
private def show_model_edit_form(model, test_callback: nil)
|
|
1707
1807
|
modal = Components::ModalComponent.new
|
|
1708
|
-
|
|
1808
|
+
|
|
1709
1809
|
is_new = model.nil?
|
|
1710
1810
|
model ||= {}
|
|
1711
|
-
|
|
1811
|
+
|
|
1712
1812
|
# For new models, show provider selection first
|
|
1713
1813
|
selected_provider = nil
|
|
1714
1814
|
if is_new
|
|
@@ -1718,32 +1818,32 @@ module Clacky
|
|
|
1718
1818
|
end
|
|
1719
1819
|
provider_choices << { name: "─" * 40, disabled: true }
|
|
1720
1820
|
provider_choices << { name: "Custom (manual configuration)", value: "custom" }
|
|
1721
|
-
|
|
1821
|
+
|
|
1722
1822
|
# Show provider selection
|
|
1723
1823
|
selected_provider = modal.show(
|
|
1724
1824
|
title: "Select Provider",
|
|
1725
1825
|
choices: provider_choices,
|
|
1726
1826
|
on_close: -> { @layout.rerender_all }
|
|
1727
1827
|
)
|
|
1728
|
-
|
|
1828
|
+
|
|
1729
1829
|
# User cancelled
|
|
1730
1830
|
return nil if selected_provider.nil?
|
|
1731
1831
|
end
|
|
1732
|
-
|
|
1832
|
+
|
|
1733
1833
|
# Prepare masked API key for display
|
|
1734
1834
|
masked_key = mask_api_key(model["api_key"])
|
|
1735
|
-
|
|
1835
|
+
|
|
1736
1836
|
# Pre-fill values from provider preset if selected
|
|
1737
1837
|
provider_preset = nil
|
|
1738
1838
|
if selected_provider && selected_provider != "custom"
|
|
1739
1839
|
provider_preset = Clacky::Providers.get(selected_provider)
|
|
1740
1840
|
end
|
|
1741
|
-
|
|
1841
|
+
|
|
1742
1842
|
# Get default values from provider or existing model
|
|
1743
1843
|
default_model = provider_preset ? provider_preset["default_model"] : model["model"]
|
|
1744
1844
|
default_base_url = provider_preset ? provider_preset["base_url"] : model["base_url"]
|
|
1745
1845
|
default_api_key = model["api_key"] || ""
|
|
1746
|
-
|
|
1846
|
+
|
|
1747
1847
|
# Define fields
|
|
1748
1848
|
fields = [
|
|
1749
1849
|
{
|
|
@@ -1763,7 +1863,7 @@ module Clacky
|
|
|
1763
1863
|
default: default_base_url || ""
|
|
1764
1864
|
}
|
|
1765
1865
|
]
|
|
1766
|
-
|
|
1866
|
+
|
|
1767
1867
|
# Create validator if test_callback provided
|
|
1768
1868
|
validator = if test_callback
|
|
1769
1869
|
lambda do |values|
|
|
@@ -1772,14 +1872,14 @@ module Clacky
|
|
|
1772
1872
|
model_name = values[:model].to_s.empty? ? model["model"] : values[:model]
|
|
1773
1873
|
base_url = values[:base_url].to_s.empty? ? model["base_url"] : values[:base_url]
|
|
1774
1874
|
anthropic_format = model["anthropic_format"] # Not editable in form, use model's value
|
|
1775
|
-
|
|
1875
|
+
|
|
1776
1876
|
test_config_values = {
|
|
1777
1877
|
"api_key" => api_key,
|
|
1778
1878
|
"model" => model_name,
|
|
1779
1879
|
"base_url" => base_url,
|
|
1780
1880
|
"anthropic_format" => anthropic_format
|
|
1781
1881
|
}
|
|
1782
|
-
|
|
1882
|
+
|
|
1783
1883
|
# For new models, require all fields
|
|
1784
1884
|
if is_new
|
|
1785
1885
|
if test_config_values["api_key"].to_s.empty?
|
|
@@ -1792,7 +1892,7 @@ module Clacky
|
|
|
1792
1892
|
return { success: false, error: "Base URL is required" }
|
|
1793
1893
|
end
|
|
1794
1894
|
end
|
|
1795
|
-
|
|
1895
|
+
|
|
1796
1896
|
# Create a temporary config for testing
|
|
1797
1897
|
temp_config = Clacky::AgentConfig.new(models: [test_config_values], current_model_index: 0)
|
|
1798
1898
|
test_callback.call(temp_config)
|
|
@@ -1800,7 +1900,7 @@ module Clacky
|
|
|
1800
1900
|
else
|
|
1801
1901
|
nil
|
|
1802
1902
|
end
|
|
1803
|
-
|
|
1903
|
+
|
|
1804
1904
|
# Determine modal title based on provider
|
|
1805
1905
|
modal_title = if is_new && selected_provider && selected_provider != "custom"
|
|
1806
1906
|
provider_name = Clacky::Providers.get(selected_provider)&.dig("name") || selected_provider
|
|
@@ -1810,7 +1910,7 @@ module Clacky
|
|
|
1810
1910
|
else
|
|
1811
1911
|
"Edit Model"
|
|
1812
1912
|
end
|
|
1813
|
-
|
|
1913
|
+
|
|
1814
1914
|
# Show modal and collect values
|
|
1815
1915
|
result = modal.show(
|
|
1816
1916
|
title: modal_title,
|
|
@@ -1818,9 +1918,9 @@ module Clacky
|
|
|
1818
1918
|
validator: validator,
|
|
1819
1919
|
on_close: -> { @layout.rerender_all }
|
|
1820
1920
|
)
|
|
1821
|
-
|
|
1921
|
+
|
|
1822
1922
|
return nil if result.nil?
|
|
1823
|
-
|
|
1923
|
+
|
|
1824
1924
|
# Merge with existing model values or provider defaults
|
|
1825
1925
|
{
|
|
1826
1926
|
api_key: result[:api_key].to_s.empty? ? model["api_key"] : result[:api_key],
|
|
@@ -1829,7 +1929,7 @@ module Clacky
|
|
|
1829
1929
|
provider: selected_provider
|
|
1830
1930
|
}
|
|
1831
1931
|
end
|
|
1832
|
-
|
|
1932
|
+
|
|
1833
1933
|
# Mask API key for display
|
|
1834
1934
|
private def mask_api_key(api_key)
|
|
1835
1935
|
if api_key && !api_key.empty?
|
data/lib/clacky/ui_interface.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Clacky
|
|
|
28
28
|
# === Status messages ===
|
|
29
29
|
def show_info(message, prefix_newline: true); end
|
|
30
30
|
def show_warning(message); end
|
|
31
|
-
def show_error(message, code: nil, top_up_url: nil); end
|
|
31
|
+
def show_error(message, code: nil, top_up_url: nil, raw_message: nil); end
|
|
32
32
|
def show_success(message); end
|
|
33
33
|
def log(message, level: :info); end
|
|
34
34
|
|
|
@@ -131,6 +131,15 @@ module Clacky
|
|
|
131
131
|
# === Blocking interaction ===
|
|
132
132
|
def request_confirmation(message, default: true); end
|
|
133
133
|
|
|
134
|
+
# Auto-approve countdown for request_user_feedback. Shows a live countdown
|
|
135
|
+
# and lets the user press a key to take over and answer. Returns :timeout
|
|
136
|
+
# when no one intervenes (agent should auto-decide and continue), or a
|
|
137
|
+
# feedback string / "" when the user steps in. Non-interactive UIs (web,
|
|
138
|
+
# json, channel) have no human watching a TTY, so they default to :timeout.
|
|
139
|
+
def request_feedback_with_countdown(seconds: 10)
|
|
140
|
+
:timeout
|
|
141
|
+
end
|
|
142
|
+
|
|
134
143
|
# === Input control (CLI layer) ===
|
|
135
144
|
def clear_input; end
|
|
136
145
|
def set_input_tips(message, type: :info); end
|
|
@@ -75,6 +75,31 @@ module Clacky
|
|
|
75
75
|
to_utf8(data)
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Decode a raw PTY byte stream to valid UTF-8, auto-detecting the source
|
|
79
|
+
# encoding. UTF-8 is tried first (Linux/macOS and modern programs); when
|
|
80
|
+
# the bytes are not valid UTF-8 they are decoded as GBK/CP936 (Simplified
|
|
81
|
+
# Chinese Windows powershell.exe / cmd.exe default output); anything that
|
|
82
|
+
# still fails is scrubbed.
|
|
83
|
+
#
|
|
84
|
+
# MUST be called on complete byte runs — callers slice on "\n" (0x0A),
|
|
85
|
+
# which is never a trailing byte of a GBK or UTF-8 multibyte sequence, so
|
|
86
|
+
# a character is never split across the boundary.
|
|
87
|
+
#
|
|
88
|
+
# @param data [String, nil] raw PTY bytes (binary/ASCII-8BIT)
|
|
89
|
+
# @return [String] valid UTF-8 string
|
|
90
|
+
def self.pty_to_utf8(data)
|
|
91
|
+
return "" if data.nil? || data.empty?
|
|
92
|
+
|
|
93
|
+
s = data.dup.force_encoding("UTF-8")
|
|
94
|
+
return s if s.valid_encoding?
|
|
95
|
+
|
|
96
|
+
data.dup
|
|
97
|
+
.force_encoding("GBK")
|
|
98
|
+
.encode("UTF-8", invalid: :replace, undef: :replace, replace: "?")
|
|
99
|
+
rescue Encoding::InvalidByteSequenceError, Encoding::UndefinedConversionError
|
|
100
|
+
to_utf8(data)
|
|
101
|
+
end
|
|
102
|
+
|
|
78
103
|
# Return an ASCII-safe UTF-8 copy of *str* suitable for security regex
|
|
79
104
|
# pattern matching. Any byte that is not valid in the source encoding, or
|
|
80
105
|
# that cannot be represented in UTF-8, is replaced with '?'. The
|
data/lib/clacky/version.rb
CHANGED