openclacky 1.3.4 → 1.3.5
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 +27 -0
- data/lib/clacky/agent/fake_tool_call_detector.rb +52 -0
- data/lib/clacky/agent/session_serializer.rb +3 -2
- data/lib/clacky/agent/tool_executor.rb +0 -12
- data/lib/clacky/agent.rb +74 -9
- data/lib/clacky/api_extension.rb +81 -0
- data/lib/clacky/api_extension_loader.rb +13 -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 +144 -9
- data/lib/clacky/server/session_registry.rb +4 -2
- data/lib/clacky/server/web_ui_controller.rb +3 -2
- 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/ui_controller.rb +140 -31
- 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 +145 -22
- data/lib/clacky/web/components/onboard.js +1 -14
- 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/version/store.js +2 -0
- data/lib/clacky/web/i18n.js +24 -1
- data/lib/clacky/web/index.html +15 -0
- data/lib/clacky/web/sessions.js +141 -51
- data/lib/clacky/web/settings.js +34 -2
- data/lib/clacky/web/ws-dispatcher.js +11 -3
- 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
|
|
@@ -874,7 +875,7 @@ module Clacky
|
|
|
874
875
|
|
|
875
876
|
# Show error message
|
|
876
877
|
# @param message [String] Error message
|
|
877
|
-
def show_error(message, code: nil, top_up_url: nil)
|
|
878
|
+
def show_error(message, code: nil, top_up_url: nil, raw_message: nil)
|
|
878
879
|
output = @renderer.render_error(message)
|
|
879
880
|
append_output(output)
|
|
880
881
|
end
|
|
@@ -1054,6 +1055,99 @@ module Clacky
|
|
|
1054
1055
|
end
|
|
1055
1056
|
end
|
|
1056
1057
|
|
|
1058
|
+
# Auto-approve countdown for request_user_feedback: show a single live
|
|
1059
|
+
# countdown line. If the user presses any key before timeout, collect
|
|
1060
|
+
# their answer and return it (intervention). Otherwise return :timeout so
|
|
1061
|
+
# the agent auto-decides and continues.
|
|
1062
|
+
# @param seconds [Integer] Countdown duration
|
|
1063
|
+
# @return [String, Symbol] feedback string, "" (bare Enter), or :timeout
|
|
1064
|
+
# Show a live single-line countdown in the output area while keeping the
|
|
1065
|
+
# normal input box usable. The agent thread blocks here until either:
|
|
1066
|
+
# * the user submits a message in the regular input box -> returns the text
|
|
1067
|
+
# * the user starts typing (cancels the auto-timeout but keeps waiting)
|
|
1068
|
+
# * the countdown reaches zero with no interaction -> returns :timeout
|
|
1069
|
+
# @param seconds [Integer] Countdown duration
|
|
1070
|
+
# @return [String, Symbol] feedback text, or :timeout
|
|
1071
|
+
def request_feedback_with_countdown(seconds: 10)
|
|
1072
|
+
theme = ThemeManager.current_theme
|
|
1073
|
+
|
|
1074
|
+
queue = Queue.new
|
|
1075
|
+
entry_id = @layout.append_output(countdown_prompt(seconds, theme))
|
|
1076
|
+
|
|
1077
|
+
session = {
|
|
1078
|
+
queue: queue,
|
|
1079
|
+
entry_id: entry_id,
|
|
1080
|
+
intervened: false,
|
|
1081
|
+
watchdog: nil
|
|
1082
|
+
}
|
|
1083
|
+
@feedback_countdown = session
|
|
1084
|
+
|
|
1085
|
+
session[:watchdog] = Thread.new do
|
|
1086
|
+
remaining = seconds.to_i
|
|
1087
|
+
while remaining.positive?
|
|
1088
|
+
break if session[:intervened]
|
|
1089
|
+
|
|
1090
|
+
@layout.replace_entry(entry_id, countdown_prompt(remaining, theme))
|
|
1091
|
+
sleep 1
|
|
1092
|
+
remaining -= 1
|
|
1093
|
+
end
|
|
1094
|
+
queue.push(:timeout) unless session[:intervened]
|
|
1095
|
+
end
|
|
1096
|
+
|
|
1097
|
+
result = queue.pop
|
|
1098
|
+
session[:watchdog].kill if session[:watchdog]&.alive?
|
|
1099
|
+
@feedback_countdown = nil
|
|
1100
|
+
@layout.remove_entry(entry_id) if entry_id
|
|
1101
|
+
@layout.recalculate_layout
|
|
1102
|
+
@layout.render_all
|
|
1103
|
+
|
|
1104
|
+
if result == :timeout
|
|
1105
|
+
append_output(theme.format_text(" No response — continuing automatically.", :thinking))
|
|
1106
|
+
elsif result.to_s.strip.empty?
|
|
1107
|
+
append_output(theme.format_text(" → (continue)", :success))
|
|
1108
|
+
result = ""
|
|
1109
|
+
end
|
|
1110
|
+
|
|
1111
|
+
result
|
|
1112
|
+
end
|
|
1113
|
+
|
|
1114
|
+
# Cancel the countdown's auto-timeout when the user starts interacting,
|
|
1115
|
+
# but keep waiting for their submitted answer. Returns true if a
|
|
1116
|
+
# countdown was active.
|
|
1117
|
+
private def intervene_feedback_countdown
|
|
1118
|
+
session = @feedback_countdown
|
|
1119
|
+
return false unless session && !session[:intervened]
|
|
1120
|
+
|
|
1121
|
+
session[:intervened] = true
|
|
1122
|
+
session[:watchdog].kill if session[:watchdog]&.alive?
|
|
1123
|
+
if session[:entry_id]
|
|
1124
|
+
@layout.remove_entry(session[:entry_id])
|
|
1125
|
+
session[:entry_id] = nil
|
|
1126
|
+
@layout.recalculate_layout
|
|
1127
|
+
@layout.render_all
|
|
1128
|
+
end
|
|
1129
|
+
true
|
|
1130
|
+
end
|
|
1131
|
+
|
|
1132
|
+
private def countdown_prompt(remaining, theme)
|
|
1133
|
+
theme.format_text(" Auto-continuing in #{remaining}s — type your answer to step in…", :info)
|
|
1134
|
+
end
|
|
1135
|
+
|
|
1136
|
+
# Whether a keystroke should count as the user stepping into a countdown.
|
|
1137
|
+
# Plain typing and pastes do; pure scroll/navigation keys do not.
|
|
1138
|
+
private def countdown_intervening_key?(key)
|
|
1139
|
+
case key
|
|
1140
|
+
when Hash
|
|
1141
|
+
key[:type] == :rapid_input
|
|
1142
|
+
when String
|
|
1143
|
+
key.length >= 1 && key.ord >= 32
|
|
1144
|
+
when :backspace, :enter
|
|
1145
|
+
true
|
|
1146
|
+
else
|
|
1147
|
+
false
|
|
1148
|
+
end
|
|
1149
|
+
end
|
|
1150
|
+
|
|
1057
1151
|
# Show diff between old and new content
|
|
1058
1152
|
# @param old_content [String] Old content
|
|
1059
1153
|
# @param new_content [String] New content
|
|
@@ -1206,7 +1300,7 @@ module Clacky
|
|
|
1206
1300
|
# Check if API key is configured and show warning if missing
|
|
1207
1301
|
private def check_api_key_configuration
|
|
1208
1302
|
config = Clacky::AgentConfig.load
|
|
1209
|
-
|
|
1303
|
+
|
|
1210
1304
|
if !config.models_configured?
|
|
1211
1305
|
show_warning("No models configured! Please run /config to set up your models and API keys.")
|
|
1212
1306
|
elsif config.api_key.nil? || config.api_key.empty?
|
|
@@ -1250,7 +1344,7 @@ module Clacky
|
|
|
1250
1344
|
while @running
|
|
1251
1345
|
# Process any pending resize events
|
|
1252
1346
|
@layout.process_pending_resize
|
|
1253
|
-
|
|
1347
|
+
|
|
1254
1348
|
key = @layout.screen.read_key(timeout: 0.1)
|
|
1255
1349
|
next unless key
|
|
1256
1350
|
|
|
@@ -1291,6 +1385,13 @@ module Clacky
|
|
|
1291
1385
|
return
|
|
1292
1386
|
end
|
|
1293
1387
|
|
|
1388
|
+
# During an auto-approve feedback countdown the normal input box stays
|
|
1389
|
+
# live; the first meaningful keystroke cancels the auto-timeout so the
|
|
1390
|
+
# user can finish typing their answer without being rushed.
|
|
1391
|
+
if @feedback_countdown && !@feedback_countdown[:intervened] && countdown_intervening_key?(key)
|
|
1392
|
+
intervene_feedback_countdown
|
|
1393
|
+
end
|
|
1394
|
+
|
|
1294
1395
|
result = @input_area.handle_key(key)
|
|
1295
1396
|
|
|
1296
1397
|
# Handle height change first
|
|
@@ -1416,6 +1517,14 @@ module Clacky
|
|
|
1416
1517
|
append_output(output)
|
|
1417
1518
|
end
|
|
1418
1519
|
|
|
1520
|
+
# If an auto-approve feedback countdown is waiting for an answer, this
|
|
1521
|
+
# submission IS that answer — deliver it to the waiting agent thread
|
|
1522
|
+
# instead of starting a brand-new turn.
|
|
1523
|
+
if (session = @feedback_countdown)
|
|
1524
|
+
session[:queue].push(data[:text].to_s)
|
|
1525
|
+
return
|
|
1526
|
+
end
|
|
1527
|
+
|
|
1419
1528
|
# Then call callback (allows interrupting previous agent before processing new input)
|
|
1420
1529
|
@input_callback&.call(data[:text], data[:files])
|
|
1421
1530
|
end
|
|
@@ -1425,31 +1534,31 @@ module Clacky
|
|
|
1425
1534
|
# @return [Hash, nil] Hash with updated config values, or nil if cancelled
|
|
1426
1535
|
public def show_config_modal(current_config, test_callback: nil)
|
|
1427
1536
|
modal = Components::ModalComponent.new
|
|
1428
|
-
|
|
1537
|
+
|
|
1429
1538
|
loop do
|
|
1430
1539
|
# Build menu choices
|
|
1431
1540
|
choices = []
|
|
1432
|
-
|
|
1541
|
+
|
|
1433
1542
|
# Add model list
|
|
1434
1543
|
current_config.models.each_with_index do |model, idx|
|
|
1435
1544
|
is_current = (idx == current_config.current_model_index)
|
|
1436
1545
|
model_name = model["model"] || "unnamed"
|
|
1437
1546
|
masked_key = mask_api_key(model["api_key"])
|
|
1438
|
-
|
|
1547
|
+
|
|
1439
1548
|
# Add type badge if present
|
|
1440
1549
|
type_badge = case model["type"]
|
|
1441
1550
|
when "default" then "[default] "
|
|
1442
1551
|
when "lite" then "[lite] "
|
|
1443
1552
|
else ""
|
|
1444
1553
|
end
|
|
1445
|
-
|
|
1554
|
+
|
|
1446
1555
|
display_name = "#{type_badge}#{model_name} (#{masked_key})"
|
|
1447
1556
|
choices << {
|
|
1448
1557
|
name: display_name,
|
|
1449
1558
|
value: { action: :switch, model_id: model["id"] }
|
|
1450
1559
|
}
|
|
1451
1560
|
end
|
|
1452
|
-
|
|
1561
|
+
|
|
1453
1562
|
# Add action buttons
|
|
1454
1563
|
choices << { name: "─" * 50, disabled: true }
|
|
1455
1564
|
choices << { name: "[+] Add New Model", value: { action: :add } }
|
|
@@ -1458,16 +1567,16 @@ module Clacky
|
|
|
1458
1567
|
choices << { name: "[-] Delete Model", value: { action: :delete } } if current_config.models.length > 1
|
|
1459
1568
|
end
|
|
1460
1569
|
choices << { name: "[X] Close", value: { action: :close } }
|
|
1461
|
-
|
|
1570
|
+
|
|
1462
1571
|
# Show menu
|
|
1463
1572
|
result = modal.show(
|
|
1464
|
-
title: "Model Configuration",
|
|
1573
|
+
title: "Model Configuration",
|
|
1465
1574
|
choices: choices,
|
|
1466
1575
|
on_close: -> { @layout.rerender_all }
|
|
1467
1576
|
)
|
|
1468
|
-
|
|
1577
|
+
|
|
1469
1578
|
return nil if result.nil?
|
|
1470
|
-
|
|
1579
|
+
|
|
1471
1580
|
case result[:action]
|
|
1472
1581
|
when :switch
|
|
1473
1582
|
# Just signal the caller which model to switch to.
|
|
@@ -1699,16 +1808,16 @@ module Clacky
|
|
|
1699
1808
|
|
|
1700
1809
|
result # Return selected task_id or nil
|
|
1701
1810
|
end
|
|
1702
|
-
|
|
1811
|
+
|
|
1703
1812
|
# Show form for editing a model
|
|
1704
1813
|
# @param model [Hash, nil] Existing model hash or nil for new model
|
|
1705
1814
|
# @return [Hash, nil] Updated model hash or nil if cancelled
|
|
1706
1815
|
private def show_model_edit_form(model, test_callback: nil)
|
|
1707
1816
|
modal = Components::ModalComponent.new
|
|
1708
|
-
|
|
1817
|
+
|
|
1709
1818
|
is_new = model.nil?
|
|
1710
1819
|
model ||= {}
|
|
1711
|
-
|
|
1820
|
+
|
|
1712
1821
|
# For new models, show provider selection first
|
|
1713
1822
|
selected_provider = nil
|
|
1714
1823
|
if is_new
|
|
@@ -1718,32 +1827,32 @@ module Clacky
|
|
|
1718
1827
|
end
|
|
1719
1828
|
provider_choices << { name: "─" * 40, disabled: true }
|
|
1720
1829
|
provider_choices << { name: "Custom (manual configuration)", value: "custom" }
|
|
1721
|
-
|
|
1830
|
+
|
|
1722
1831
|
# Show provider selection
|
|
1723
1832
|
selected_provider = modal.show(
|
|
1724
1833
|
title: "Select Provider",
|
|
1725
1834
|
choices: provider_choices,
|
|
1726
1835
|
on_close: -> { @layout.rerender_all }
|
|
1727
1836
|
)
|
|
1728
|
-
|
|
1837
|
+
|
|
1729
1838
|
# User cancelled
|
|
1730
1839
|
return nil if selected_provider.nil?
|
|
1731
1840
|
end
|
|
1732
|
-
|
|
1841
|
+
|
|
1733
1842
|
# Prepare masked API key for display
|
|
1734
1843
|
masked_key = mask_api_key(model["api_key"])
|
|
1735
|
-
|
|
1844
|
+
|
|
1736
1845
|
# Pre-fill values from provider preset if selected
|
|
1737
1846
|
provider_preset = nil
|
|
1738
1847
|
if selected_provider && selected_provider != "custom"
|
|
1739
1848
|
provider_preset = Clacky::Providers.get(selected_provider)
|
|
1740
1849
|
end
|
|
1741
|
-
|
|
1850
|
+
|
|
1742
1851
|
# Get default values from provider or existing model
|
|
1743
1852
|
default_model = provider_preset ? provider_preset["default_model"] : model["model"]
|
|
1744
1853
|
default_base_url = provider_preset ? provider_preset["base_url"] : model["base_url"]
|
|
1745
1854
|
default_api_key = model["api_key"] || ""
|
|
1746
|
-
|
|
1855
|
+
|
|
1747
1856
|
# Define fields
|
|
1748
1857
|
fields = [
|
|
1749
1858
|
{
|
|
@@ -1763,7 +1872,7 @@ module Clacky
|
|
|
1763
1872
|
default: default_base_url || ""
|
|
1764
1873
|
}
|
|
1765
1874
|
]
|
|
1766
|
-
|
|
1875
|
+
|
|
1767
1876
|
# Create validator if test_callback provided
|
|
1768
1877
|
validator = if test_callback
|
|
1769
1878
|
lambda do |values|
|
|
@@ -1772,14 +1881,14 @@ module Clacky
|
|
|
1772
1881
|
model_name = values[:model].to_s.empty? ? model["model"] : values[:model]
|
|
1773
1882
|
base_url = values[:base_url].to_s.empty? ? model["base_url"] : values[:base_url]
|
|
1774
1883
|
anthropic_format = model["anthropic_format"] # Not editable in form, use model's value
|
|
1775
|
-
|
|
1884
|
+
|
|
1776
1885
|
test_config_values = {
|
|
1777
1886
|
"api_key" => api_key,
|
|
1778
1887
|
"model" => model_name,
|
|
1779
1888
|
"base_url" => base_url,
|
|
1780
1889
|
"anthropic_format" => anthropic_format
|
|
1781
1890
|
}
|
|
1782
|
-
|
|
1891
|
+
|
|
1783
1892
|
# For new models, require all fields
|
|
1784
1893
|
if is_new
|
|
1785
1894
|
if test_config_values["api_key"].to_s.empty?
|
|
@@ -1792,7 +1901,7 @@ module Clacky
|
|
|
1792
1901
|
return { success: false, error: "Base URL is required" }
|
|
1793
1902
|
end
|
|
1794
1903
|
end
|
|
1795
|
-
|
|
1904
|
+
|
|
1796
1905
|
# Create a temporary config for testing
|
|
1797
1906
|
temp_config = Clacky::AgentConfig.new(models: [test_config_values], current_model_index: 0)
|
|
1798
1907
|
test_callback.call(temp_config)
|
|
@@ -1800,7 +1909,7 @@ module Clacky
|
|
|
1800
1909
|
else
|
|
1801
1910
|
nil
|
|
1802
1911
|
end
|
|
1803
|
-
|
|
1912
|
+
|
|
1804
1913
|
# Determine modal title based on provider
|
|
1805
1914
|
modal_title = if is_new && selected_provider && selected_provider != "custom"
|
|
1806
1915
|
provider_name = Clacky::Providers.get(selected_provider)&.dig("name") || selected_provider
|
|
@@ -1810,7 +1919,7 @@ module Clacky
|
|
|
1810
1919
|
else
|
|
1811
1920
|
"Edit Model"
|
|
1812
1921
|
end
|
|
1813
|
-
|
|
1922
|
+
|
|
1814
1923
|
# Show modal and collect values
|
|
1815
1924
|
result = modal.show(
|
|
1816
1925
|
title: modal_title,
|
|
@@ -1818,9 +1927,9 @@ module Clacky
|
|
|
1818
1927
|
validator: validator,
|
|
1819
1928
|
on_close: -> { @layout.rerender_all }
|
|
1820
1929
|
)
|
|
1821
|
-
|
|
1930
|
+
|
|
1822
1931
|
return nil if result.nil?
|
|
1823
|
-
|
|
1932
|
+
|
|
1824
1933
|
# Merge with existing model values or provider defaults
|
|
1825
1934
|
{
|
|
1826
1935
|
api_key: result[:api_key].to_s.empty? ? model["api_key"] : result[:api_key],
|
|
@@ -1829,7 +1938,7 @@ module Clacky
|
|
|
1829
1938
|
provider: selected_provider
|
|
1830
1939
|
}
|
|
1831
1940
|
end
|
|
1832
|
-
|
|
1941
|
+
|
|
1833
1942
|
# Mask API key for display
|
|
1834
1943
|
private def mask_api_key(api_key)
|
|
1835
1944
|
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
data/lib/clacky/web/app.css
CHANGED
|
@@ -650,38 +650,38 @@ body {
|
|
|
650
650
|
position: relative;
|
|
651
651
|
display: flex;
|
|
652
652
|
align-items: center;
|
|
653
|
-
border-radius:
|
|
653
|
+
border-radius: 6px;
|
|
654
654
|
overflow: visible;
|
|
655
655
|
}
|
|
656
656
|
.btn-split-main {
|
|
657
|
-
height: 1.
|
|
658
|
-
padding: 0 0.
|
|
657
|
+
height: 1.5rem;
|
|
658
|
+
padding: 0 0.625rem;
|
|
659
659
|
border: none;
|
|
660
|
-
border-radius:
|
|
660
|
+
border-radius: 6px 0 0 6px;
|
|
661
661
|
background: var(--color-accent-primary);
|
|
662
662
|
color: #fff;
|
|
663
|
-
font-size: 0.
|
|
663
|
+
font-size: 0.75rem;
|
|
664
664
|
font-weight: 500;
|
|
665
665
|
white-space: nowrap;
|
|
666
666
|
line-height: 1;
|
|
667
667
|
cursor: pointer;
|
|
668
|
-
transition: background 0.15s,
|
|
668
|
+
transition: background 0.15s, box-shadow 0.15s;
|
|
669
669
|
}
|
|
670
670
|
.btn-split-main:hover {
|
|
671
671
|
background: var(--color-button-primary-hover);
|
|
672
|
-
box-shadow: 0 2px
|
|
672
|
+
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
|
|
673
673
|
}
|
|
674
674
|
.btn-split-arrow {
|
|
675
|
-
height: 1.
|
|
676
|
-
padding: 0 0.
|
|
675
|
+
height: 1.5rem;
|
|
676
|
+
padding: 0 0.35rem;
|
|
677
677
|
border: none;
|
|
678
678
|
border-left: 1px solid rgba(255,255,255,0.3);
|
|
679
|
-
border-radius: 0
|
|
679
|
+
border-radius: 0 6px 6px 0;
|
|
680
680
|
background: var(--color-accent-primary);
|
|
681
681
|
color: #fff;
|
|
682
|
-
font-size: 0.
|
|
682
|
+
font-size: 0.75rem;
|
|
683
683
|
cursor: pointer;
|
|
684
|
-
transition: background 0.15s
|
|
684
|
+
transition: background 0.15s;
|
|
685
685
|
}
|
|
686
686
|
.btn-split-arrow:hover {
|
|
687
687
|
background: var(--color-button-primary-hover);
|
|
@@ -690,7 +690,7 @@ body {
|
|
|
690
690
|
/* ── Dropdown menu ───────────────────────────────────────────────────────── */
|
|
691
691
|
.new-session-dropdown {
|
|
692
692
|
position: absolute;
|
|
693
|
-
top:
|
|
693
|
+
top: 100%;
|
|
694
694
|
right: 0;
|
|
695
695
|
min-width: 11.25rem;
|
|
696
696
|
background: var(--color-bg-card);
|
|
@@ -698,13 +698,13 @@ body {
|
|
|
698
698
|
border-radius: 7px;
|
|
699
699
|
box-shadow: var(--shadow-md);
|
|
700
700
|
z-index: 200;
|
|
701
|
-
padding: 0.
|
|
701
|
+
padding: 0.375rem 0 0.25rem;
|
|
702
702
|
overflow: hidden;
|
|
703
703
|
}
|
|
704
704
|
.new-session-dropdown[hidden] { display: none; }
|
|
705
705
|
.dropdown-item {
|
|
706
|
-
padding: 0.
|
|
707
|
-
font-size: 0.
|
|
706
|
+
padding: 0.4rem 0.875rem;
|
|
707
|
+
font-size: 0.8125rem;
|
|
708
708
|
color: var(--color-text-secondary);
|
|
709
709
|
cursor: pointer;
|
|
710
710
|
white-space: nowrap;
|
|
@@ -1799,7 +1799,7 @@ body {
|
|
|
1799
1799
|
white-space: nowrap;
|
|
1800
1800
|
transition: background .12s;
|
|
1801
1801
|
}
|
|
1802
|
-
.task-run-btn:hover { background: var(--color-
|
|
1802
|
+
.task-run-btn:hover { background: var(--color-button-primary-hover); }
|
|
1803
1803
|
.task-action-btn {
|
|
1804
1804
|
display: inline-flex;
|
|
1805
1805
|
align-items: center;
|
|
@@ -2456,6 +2456,36 @@ body {
|
|
|
2456
2456
|
.msg-copy-btn { opacity: 0.65; }
|
|
2457
2457
|
}
|
|
2458
2458
|
|
|
2459
|
+
/* ── Inline "start" button on unchecked task-list items ──────────────────── */
|
|
2460
|
+
.msg-assistant li.task-list-item { position: relative; }
|
|
2461
|
+
.msg-todo-spawn {
|
|
2462
|
+
display: inline-flex;
|
|
2463
|
+
align-items: center;
|
|
2464
|
+
margin-left: 0.5rem;
|
|
2465
|
+
padding: 0.05rem 0.5rem;
|
|
2466
|
+
font-size: 0.75rem;
|
|
2467
|
+
line-height: 1.4;
|
|
2468
|
+
border: 1px solid var(--color-border-primary);
|
|
2469
|
+
border-radius: 5px;
|
|
2470
|
+
background: var(--color-bg-primary);
|
|
2471
|
+
color: var(--color-text-secondary);
|
|
2472
|
+
cursor: pointer;
|
|
2473
|
+
opacity: 0;
|
|
2474
|
+
vertical-align: middle;
|
|
2475
|
+
transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
|
2476
|
+
}
|
|
2477
|
+
.msg-assistant:hover .msg-todo-spawn,
|
|
2478
|
+
.msg-todo-spawn:focus-visible { opacity: 1; }
|
|
2479
|
+
.msg-todo-spawn:hover {
|
|
2480
|
+
color: var(--color-text-primary);
|
|
2481
|
+
background: var(--color-bg-tertiary);
|
|
2482
|
+
border-color: var(--color-border-secondary);
|
|
2483
|
+
}
|
|
2484
|
+
.msg-todo-spawn:disabled { opacity: 0.5; cursor: default; }
|
|
2485
|
+
@media (hover: none) {
|
|
2486
|
+
.msg-todo-spawn { opacity: 0.7; }
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2459
2489
|
/* ── Markdown rendering inside assistant messages ────────────────────────── */
|
|
2460
2490
|
.msg-assistant p:last-child { margin-bottom: 0; }
|
|
2461
2491
|
.msg-assistant h1, .msg-assistant h2, .msg-assistant h3,
|
|
@@ -2690,7 +2720,7 @@ body {
|
|
|
2690
2720
|
align-self: flex-start;
|
|
2691
2721
|
display: flex;
|
|
2692
2722
|
flex-direction: column;
|
|
2693
|
-
gap: 0.
|
|
2723
|
+
gap: 0.5rem;
|
|
2694
2724
|
}
|
|
2695
2725
|
.msg-error .retry-btn {
|
|
2696
2726
|
align-self: flex-start;
|
|
@@ -2721,6 +2751,56 @@ body {
|
|
|
2721
2751
|
.msg-error a:hover {
|
|
2722
2752
|
opacity: 0.8;
|
|
2723
2753
|
}
|
|
2754
|
+
.error-raw-detail {
|
|
2755
|
+
margin-top: -0.125rem;
|
|
2756
|
+
font-size: 0.75rem;
|
|
2757
|
+
align-self: flex-start;
|
|
2758
|
+
max-width: 100%;
|
|
2759
|
+
}
|
|
2760
|
+
.error-raw-detail summary {
|
|
2761
|
+
cursor: pointer;
|
|
2762
|
+
font-size: 0.7rem;
|
|
2763
|
+
font-weight: 500;
|
|
2764
|
+
user-select: none;
|
|
2765
|
+
color: var(--color-error);
|
|
2766
|
+
opacity: 0.6;
|
|
2767
|
+
list-style: none;
|
|
2768
|
+
display: inline-flex;
|
|
2769
|
+
align-items: center;
|
|
2770
|
+
gap: 0.25rem;
|
|
2771
|
+
transition: opacity 0.15s;
|
|
2772
|
+
}
|
|
2773
|
+
.error-raw-detail summary::-webkit-details-marker { display: none; }
|
|
2774
|
+
.error-raw-detail summary::before {
|
|
2775
|
+
content: "";
|
|
2776
|
+
display: inline-block;
|
|
2777
|
+
width: 0;
|
|
2778
|
+
height: 0;
|
|
2779
|
+
border-top: 4px solid transparent;
|
|
2780
|
+
border-bottom: 4px solid transparent;
|
|
2781
|
+
border-left: 5px solid currentColor;
|
|
2782
|
+
transition: transform 0.15s;
|
|
2783
|
+
flex-shrink: 0;
|
|
2784
|
+
}
|
|
2785
|
+
.error-raw-detail[open] summary::before {
|
|
2786
|
+
transform: rotate(90deg);
|
|
2787
|
+
}
|
|
2788
|
+
.error-raw-detail summary:hover {
|
|
2789
|
+
opacity: 0.9;
|
|
2790
|
+
}
|
|
2791
|
+
.error-raw-detail pre {
|
|
2792
|
+
margin: 0.375rem 0 0;
|
|
2793
|
+
padding: 0.5rem 0.625rem;
|
|
2794
|
+
background: rgba(255, 59, 48, 0.05);
|
|
2795
|
+
border: 1px solid rgba(255, 59, 48, 0.12);
|
|
2796
|
+
border-radius: 5px;
|
|
2797
|
+
font-family: var(--font-mono, monospace);
|
|
2798
|
+
font-size: 0.7rem;
|
|
2799
|
+
line-height: 1.5;
|
|
2800
|
+
white-space: pre-wrap;
|
|
2801
|
+
word-break: break-all;
|
|
2802
|
+
color: var(--color-text-secondary);
|
|
2803
|
+
}
|
|
2724
2804
|
.msg-success { color: var(--color-success); align-self: flex-start; font-size: 0.8125rem; }
|
|
2725
2805
|
.tool-name { color: var(--color-warning); font-weight: 600; }
|
|
2726
2806
|
.progress-msg { color: var(--color-accent-primary); font-size: 0.75rem; align-self: center; }
|
|
@@ -6929,10 +7009,10 @@ body {
|
|
|
6929
7009
|
background: var(--color-bg-hover);
|
|
6930
7010
|
}
|
|
6931
7011
|
.code-editor-body .cm-activeLine {
|
|
6932
|
-
background: var(--color-bg-hover);
|
|
7012
|
+
background: color-mix(in srgb, var(--color-bg-hover) 60%, transparent);
|
|
6933
7013
|
}
|
|
6934
7014
|
.code-editor-body .cm-selectionBackground {
|
|
6935
|
-
background: color-mix(in srgb, var(--color-accent-primary)
|
|
7015
|
+
background: color-mix(in srgb, var(--color-accent-primary) 40%, transparent) !important;
|
|
6936
7016
|
}
|
|
6937
7017
|
.code-editor-footer {
|
|
6938
7018
|
display: flex;
|
|
@@ -10046,7 +10126,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
10046
10126
|
border-right: 1px solid var(--color-border-primary);
|
|
10047
10127
|
}
|
|
10048
10128
|
.billing-period-btn:hover {
|
|
10049
|
-
background: var(--color-bg-
|
|
10129
|
+
background: var(--color-bg-hover);
|
|
10050
10130
|
color: var(--color-text-primary);
|
|
10051
10131
|
}
|
|
10052
10132
|
.billing-period-btn.active {
|
|
@@ -10906,7 +10986,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
10906
10986
|
white-space: nowrap;
|
|
10907
10987
|
transition: opacity 0.15s ease, transform 0.05s ease;
|
|
10908
10988
|
}
|
|
10909
|
-
.btn-mcp-cta:hover {
|
|
10989
|
+
.btn-mcp-cta:hover { background: var(--color-button-primary-hover) }
|
|
10910
10990
|
.btn-mcp-cta:active { transform: scale(0.98); }
|
|
10911
10991
|
.btn-mcp-cta-large {
|
|
10912
10992
|
padding: 0.625rem 1.125rem;
|
|
@@ -11019,6 +11099,49 @@ body.setup-mode[data-theme="dark"] {
|
|
|
11019
11099
|
color: var(--color-error, #d33);
|
|
11020
11100
|
}
|
|
11021
11101
|
|
|
11102
|
+
/* ── Accent Color Swatches ───────────────────────────────────────────── */
|
|
11103
|
+
.settings-accent-swatches {
|
|
11104
|
+
display: flex;
|
|
11105
|
+
flex-wrap: wrap;
|
|
11106
|
+
gap: 0.5rem;
|
|
11107
|
+
padding: 0.25rem 0;
|
|
11108
|
+
}
|
|
11109
|
+
|
|
11110
|
+
.settings-accent-swatch {
|
|
11111
|
+
width: 2.25rem;
|
|
11112
|
+
height: 2.25rem;
|
|
11113
|
+
border-radius: 50%;
|
|
11114
|
+
border: none;
|
|
11115
|
+
cursor: pointer;
|
|
11116
|
+
padding: 0;
|
|
11117
|
+
transition: transform 0.15s;
|
|
11118
|
+
outline: none;
|
|
11119
|
+
position: relative;
|
|
11120
|
+
}
|
|
11121
|
+
|
|
11122
|
+
.settings-accent-swatch:hover {
|
|
11123
|
+
transform: scale(1.15);
|
|
11124
|
+
}
|
|
11125
|
+
|
|
11126
|
+
.settings-accent-swatch.active::after {
|
|
11127
|
+
content: "";
|
|
11128
|
+
position: absolute;
|
|
11129
|
+
width: 0.375rem;
|
|
11130
|
+
height: 0.625rem;
|
|
11131
|
+
border-right: 0.125rem solid white;
|
|
11132
|
+
border-bottom: 0.125rem solid white;
|
|
11133
|
+
top: 46%;
|
|
11134
|
+
left: 50%;
|
|
11135
|
+
transform: translate(-50%, -50%) rotate(45deg);
|
|
11136
|
+
}
|
|
11137
|
+
|
|
11138
|
+
.swatch-indigo { background: #4f46e5; }
|
|
11139
|
+
.swatch-aurora-blue { background: #3B82F6; }
|
|
11140
|
+
.swatch-forest-green { background: #10B981; }
|
|
11141
|
+
.swatch-sunrise-orange { background: #F59E0B; }
|
|
11142
|
+
.swatch-rose-violet { background: #8B5CF6; }
|
|
11143
|
+
.swatch-coral-red { background: #EF4444; }
|
|
11144
|
+
|
|
11022
11145
|
/* ── Sessions List ───────────────────────────────────────────────────── */
|
|
11023
11146
|
.billing-sessions-row {
|
|
11024
11147
|
margin-top: 1rem;
|