openclacky 0.9.30 → 0.9.32
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 +38 -0
- data/lib/clacky/agent/llm_caller.rb +5 -5
- data/lib/clacky/agent/memory_updater.rb +1 -1
- data/lib/clacky/agent/session_serializer.rb +2 -1
- data/lib/clacky/agent/skill_auto_creator.rb +119 -0
- data/lib/clacky/agent/skill_evolution.rb +46 -0
- data/lib/clacky/agent/skill_manager.rb +8 -0
- data/lib/clacky/agent/skill_reflector.rb +97 -0
- data/lib/clacky/agent.rb +38 -12
- data/lib/clacky/agent_config.rb +10 -1
- data/lib/clacky/brand_config.rb +23 -0
- data/lib/clacky/cli.rb +1 -1
- data/lib/clacky/default_skills/onboard/SKILL.md +15 -7
- data/lib/clacky/default_skills/personal-website/publish.rb +1 -1
- data/lib/clacky/default_skills/skill-creator/SKILL.md +46 -0
- data/lib/clacky/json_ui_controller.rb +0 -4
- data/lib/clacky/message_history.rb +0 -12
- data/lib/clacky/plain_ui_controller.rb +19 -1
- data/lib/clacky/platform_http_client.rb +2 -4
- data/lib/clacky/providers.rb +12 -1
- data/lib/clacky/server/channel/channel_ui_controller.rb +0 -2
- data/lib/clacky/server/http_server.rb +13 -1
- data/lib/clacky/server/web_ui_controller.rb +55 -29
- data/lib/clacky/tools/shell.rb +91 -170
- data/lib/clacky/ui2/ui_controller.rb +100 -93
- data/lib/clacky/ui_interface.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +5 -2
- data/lib/clacky/utils/limit_stack.rb +81 -13
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +247 -51
- data/lib/clacky/web/app.js +11 -3
- data/lib/clacky/web/brand.js +21 -3
- data/lib/clacky/web/creator.js +13 -2
- data/lib/clacky/web/i18n.js +41 -15
- data/lib/clacky/web/index.html +38 -20
- data/lib/clacky/web/sessions.js +256 -57
- data/lib/clacky/web/settings.js +32 -0
- data/lib/clacky/web/skills.js +61 -1
- metadata +4 -1
|
@@ -113,7 +113,7 @@ module Clacky
|
|
|
113
113
|
def self.raise_helpful_error(call, tool_registry, original_error)
|
|
114
114
|
tool = tool_registry.get(call[:name])
|
|
115
115
|
error_msg = build_error_message(call, tool, original_error)
|
|
116
|
-
raise
|
|
116
|
+
raise BadArgumentsError, error_msg
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def self.build_error_message(call, tool, original_error)
|
|
@@ -173,8 +173,11 @@ module Clacky
|
|
|
173
173
|
end
|
|
174
174
|
end
|
|
175
175
|
|
|
176
|
+
# Raised when tool call arguments are malformed or missing required params.
|
|
177
|
+
class BadArgumentsError < StandardError; end
|
|
178
|
+
|
|
176
179
|
# Custom exception for missing required parameters
|
|
177
|
-
class MissingRequiredParamsError <
|
|
180
|
+
class MissingRequiredParamsError < BadArgumentsError
|
|
178
181
|
attr_reader :tool_name, :missing_params, :provided_params
|
|
179
182
|
|
|
180
183
|
def initialize(tool_name, missing_params, provided_params)
|
|
@@ -2,21 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
module Clacky
|
|
4
4
|
module Utils
|
|
5
|
-
# Auto-rolling fixed-size array
|
|
6
|
-
# Automatically discards oldest elements when
|
|
5
|
+
# Auto-rolling fixed-size array.
|
|
6
|
+
# Automatically discards oldest elements when the line-count limit is exceeded.
|
|
7
|
+
#
|
|
8
|
+
# Optional limits (all default to nil = no limit):
|
|
9
|
+
# max_line_chars – truncate each individual line to this many characters on push
|
|
10
|
+
# max_chars – once the total accepted chars reach this threshold, further
|
|
11
|
+
# pushes are silently dropped (sets #truncated? = true)
|
|
12
|
+
#
|
|
13
|
+
# These extra limits are fully opt-in; existing callers that only pass max_size
|
|
14
|
+
# are completely unaffected.
|
|
7
15
|
class LimitStack
|
|
8
16
|
attr_reader :max_size, :items
|
|
9
17
|
|
|
10
|
-
def initialize(max_size: 5000)
|
|
11
|
-
@max_size
|
|
12
|
-
@
|
|
18
|
+
def initialize(max_size: 5000, max_line_chars: nil, max_chars: nil)
|
|
19
|
+
@max_size = max_size
|
|
20
|
+
@max_line_chars = max_line_chars
|
|
21
|
+
@max_chars = max_chars
|
|
22
|
+
|
|
23
|
+
@items = []
|
|
24
|
+
@total_chars = 0 # chars currently stored in @items
|
|
25
|
+
@truncated = false
|
|
26
|
+
@chars_full = false # latched true once max_chars is reached
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# True if any content was dropped (lines rolled off the front OR
|
|
30
|
+
# chars budget was exceeded OR a line was truncated).
|
|
31
|
+
def truncated?
|
|
32
|
+
@truncated
|
|
13
33
|
end
|
|
14
34
|
|
|
15
35
|
# Add elements (supports single or multiple)
|
|
16
36
|
def push(*elements)
|
|
17
37
|
elements.each do |element|
|
|
18
|
-
|
|
19
|
-
trim_if_needed
|
|
38
|
+
_push_one(element)
|
|
20
39
|
end
|
|
21
40
|
self
|
|
22
41
|
end
|
|
@@ -27,13 +46,15 @@ module Clacky
|
|
|
27
46
|
return self if text.nil? || text.empty?
|
|
28
47
|
|
|
29
48
|
lines = text.is_a?(Array) ? text : text.lines
|
|
30
|
-
lines.each { |line|
|
|
49
|
+
lines.each { |line| _push_one(line) }
|
|
31
50
|
self
|
|
32
51
|
end
|
|
33
52
|
|
|
34
53
|
# Remove and return the last element
|
|
35
54
|
def pop
|
|
36
|
-
@items.pop
|
|
55
|
+
item = @items.pop
|
|
56
|
+
@total_chars -= item.length if item.is_a?(String)
|
|
57
|
+
item
|
|
37
58
|
end
|
|
38
59
|
|
|
39
60
|
# Get last N elements
|
|
@@ -64,6 +85,9 @@ module Clacky
|
|
|
64
85
|
# Clear all elements
|
|
65
86
|
def clear
|
|
66
87
|
@items.clear
|
|
88
|
+
@total_chars = 0
|
|
89
|
+
@truncated = false
|
|
90
|
+
@chars_full = false
|
|
67
91
|
self
|
|
68
92
|
end
|
|
69
93
|
|
|
@@ -72,12 +96,56 @@ module Clacky
|
|
|
72
96
|
@items.each(&block)
|
|
73
97
|
end
|
|
74
98
|
|
|
75
|
-
|
|
99
|
+
# kept for compatibility (called internally; public so subclasses can override)
|
|
76
100
|
def trim_if_needed
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
@
|
|
101
|
+
while @items.size > @max_size
|
|
102
|
+
removed = @items.shift
|
|
103
|
+
@total_chars -= removed.length if removed.is_a?(String)
|
|
104
|
+
@truncated = true
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private def _push_one(element)
|
|
109
|
+
# --- chars budget check ---
|
|
110
|
+
if @chars_full
|
|
111
|
+
@truncated = true
|
|
112
|
+
return
|
|
80
113
|
end
|
|
114
|
+
|
|
115
|
+
item = element
|
|
116
|
+
|
|
117
|
+
# --- per-line truncation ---
|
|
118
|
+
if @max_line_chars && item.is_a?(String) && item.length > @max_line_chars
|
|
119
|
+
item = item[0, @max_line_chars]
|
|
120
|
+
# Preserve trailing newline if original had one
|
|
121
|
+
item += "\n" if element.end_with?("\n") && !item.end_with?("\n")
|
|
122
|
+
@truncated = true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# --- total chars check ---
|
|
126
|
+
if @max_chars && item.is_a?(String)
|
|
127
|
+
remaining = @max_chars - @total_chars
|
|
128
|
+
if remaining <= 0
|
|
129
|
+
@chars_full = true
|
|
130
|
+
@truncated = true
|
|
131
|
+
return
|
|
132
|
+
end
|
|
133
|
+
if item.length > remaining
|
|
134
|
+
# If original line ends with \n we must preserve it, so reserve 1
|
|
135
|
+
# byte for it — this keeps total_chars strictly within max_chars.
|
|
136
|
+
needs_newline = element.is_a?(String) && element.end_with?("\n")
|
|
137
|
+
cut = needs_newline ? [remaining - 1, 0].max : remaining
|
|
138
|
+
item = item[0, cut]
|
|
139
|
+
item += "\n" if needs_newline && !item.end_with?("\n")
|
|
140
|
+
@chars_full = true
|
|
141
|
+
@truncated = true
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@items << item
|
|
146
|
+
@total_chars += item.length if item.is_a?(String)
|
|
147
|
+
|
|
148
|
+
trim_if_needed
|
|
81
149
|
end
|
|
82
150
|
end
|
|
83
151
|
end
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky/web/app.css
CHANGED
|
@@ -181,7 +181,6 @@ body {
|
|
|
181
181
|
overflow: hidden;
|
|
182
182
|
text-overflow: ellipsis;
|
|
183
183
|
max-width: 180px;
|
|
184
|
-
line-height: 1;
|
|
185
184
|
}
|
|
186
185
|
/* When a logo image is present, slightly dim the text to act as a subtitle */
|
|
187
186
|
#header-brand.has-logo .header-logo {
|
|
@@ -311,8 +310,9 @@ body {
|
|
|
311
310
|
display: flex;
|
|
312
311
|
flex-direction: column;
|
|
313
312
|
min-height: 0;
|
|
313
|
+
padding-bottom: 10px;
|
|
314
314
|
}
|
|
315
|
-
#session-list { padding:
|
|
315
|
+
#session-list { padding: 6px 8px 8px; min-height: 108px; }
|
|
316
316
|
|
|
317
317
|
/* ── Sidebar divider (Section Labels) ───────────────────────────────────── */
|
|
318
318
|
.sidebar-divider {
|
|
@@ -327,6 +327,10 @@ body {
|
|
|
327
327
|
text-transform: uppercase;
|
|
328
328
|
letter-spacing: 1px;
|
|
329
329
|
margin-top: 4px;
|
|
330
|
+
position: sticky;
|
|
331
|
+
top: 0;
|
|
332
|
+
background: var(--color-bg-secondary);
|
|
333
|
+
z-index: 10;
|
|
330
334
|
}
|
|
331
335
|
.sidebar-divider:first-child {
|
|
332
336
|
margin-top: 0;
|
|
@@ -348,34 +352,33 @@ body {
|
|
|
348
352
|
padding: 0 8px;
|
|
349
353
|
border: none;
|
|
350
354
|
border-radius: 5px 0 0 5px;
|
|
351
|
-
background: var(--color-
|
|
352
|
-
color:
|
|
355
|
+
background: var(--color-accent-primary);
|
|
356
|
+
color: #fff;
|
|
353
357
|
font-size: 11px;
|
|
354
358
|
font-weight: 500;
|
|
355
359
|
white-space: nowrap;
|
|
356
360
|
line-height: 1;
|
|
357
361
|
cursor: pointer;
|
|
358
|
-
transition: background 0.15s, color 0.15s;
|
|
362
|
+
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
|
|
359
363
|
}
|
|
360
364
|
.btn-split-main:hover {
|
|
361
|
-
background: var(--color-
|
|
362
|
-
|
|
365
|
+
background: var(--color-button-primary-hover);
|
|
366
|
+
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
363
367
|
}
|
|
364
368
|
.btn-split-arrow {
|
|
365
369
|
height: 22px;
|
|
366
370
|
padding: 0 4px;
|
|
367
371
|
border: none;
|
|
368
|
-
border-left: 1px solid
|
|
372
|
+
border-left: 1px solid rgba(255,255,255,0.3);
|
|
369
373
|
border-radius: 0 5px 5px 0;
|
|
370
|
-
background: var(--color-
|
|
371
|
-
color:
|
|
374
|
+
background: var(--color-accent-primary);
|
|
375
|
+
color: #fff;
|
|
372
376
|
font-size: 10px;
|
|
373
377
|
cursor: pointer;
|
|
374
378
|
transition: background 0.15s, color 0.15s;
|
|
375
379
|
}
|
|
376
380
|
.btn-split-arrow:hover {
|
|
377
|
-
background: var(--color-
|
|
378
|
-
color: #fff;
|
|
381
|
+
background: var(--color-button-primary-hover);
|
|
379
382
|
}
|
|
380
383
|
|
|
381
384
|
/* ── Dropdown menu ───────────────────────────────────────────────────────── */
|
|
@@ -1513,6 +1516,65 @@ body {
|
|
|
1513
1516
|
.msg-assistant em { font-style: italic; color: var(--color-text-secondary); }
|
|
1514
1517
|
.msg-tool { background: var(--color-bg-primary); border: 1px solid var(--color-border-primary); font-family: monospace; font-size: 12px; color: var(--color-text-secondary); align-self: flex-start; }
|
|
1515
1518
|
.msg-info { color: var(--color-text-secondary); font-size: 12px; align-self: center; font-style: italic; }
|
|
1519
|
+
|
|
1520
|
+
/* ── Feedback request card ──────────────────────────────────────────────── */
|
|
1521
|
+
.feedback-card {
|
|
1522
|
+
background: var(--color-bg-secondary);
|
|
1523
|
+
border: 1px solid var(--color-border-primary);
|
|
1524
|
+
border-radius: 12px;
|
|
1525
|
+
padding: 16px 18px;
|
|
1526
|
+
margin: 4px 0;
|
|
1527
|
+
align-self: flex-start;
|
|
1528
|
+
max-width: 85%;
|
|
1529
|
+
color: var(--color-text-primary);
|
|
1530
|
+
}
|
|
1531
|
+
.feedback-context {
|
|
1532
|
+
font-size: 12px;
|
|
1533
|
+
color: var(--color-text-secondary);
|
|
1534
|
+
margin-bottom: 8px;
|
|
1535
|
+
line-height: 1.4;
|
|
1536
|
+
}
|
|
1537
|
+
.feedback-question {
|
|
1538
|
+
font-size: 14px;
|
|
1539
|
+
font-weight: 500;
|
|
1540
|
+
color: var(--color-text-primary);
|
|
1541
|
+
margin-bottom: 12px;
|
|
1542
|
+
line-height: 1.5;
|
|
1543
|
+
}
|
|
1544
|
+
.feedback-options {
|
|
1545
|
+
display: flex;
|
|
1546
|
+
flex-direction: column;
|
|
1547
|
+
gap: 6px;
|
|
1548
|
+
margin-bottom: 10px;
|
|
1549
|
+
}
|
|
1550
|
+
.feedback-option-btn {
|
|
1551
|
+
background: var(--color-bg-primary);
|
|
1552
|
+
border: 1px solid var(--color-border-primary);
|
|
1553
|
+
border-radius: 6px;
|
|
1554
|
+
padding: 7px 12px;
|
|
1555
|
+
color: var(--color-text-primary);
|
|
1556
|
+
font-size: 13px;
|
|
1557
|
+
cursor: pointer;
|
|
1558
|
+
transition: border-color 0.15s, background 0.15s;
|
|
1559
|
+
text-align: left;
|
|
1560
|
+
}
|
|
1561
|
+
.feedback-option-btn:hover {
|
|
1562
|
+
border-color: var(--color-accent-primary);
|
|
1563
|
+
background: var(--color-bg-secondary);
|
|
1564
|
+
}
|
|
1565
|
+
.feedback-option-btn:active {
|
|
1566
|
+
opacity: 0.8;
|
|
1567
|
+
}
|
|
1568
|
+
.feedback-hint {
|
|
1569
|
+
font-size: 11px;
|
|
1570
|
+
color: var(--color-text-secondary);
|
|
1571
|
+
margin-top: 6px;
|
|
1572
|
+
}
|
|
1573
|
+
.feedback-card--submitted {
|
|
1574
|
+
opacity: 0.5;
|
|
1575
|
+
pointer-events: none;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1516
1578
|
.history-start-marker {
|
|
1517
1579
|
display: flex;
|
|
1518
1580
|
align-items: center;
|
|
@@ -2926,10 +2988,26 @@ body {
|
|
|
2926
2988
|
/* ── Skills Tabs ─────────────────────────────────────────────────────────── */
|
|
2927
2989
|
#skills-tabs {
|
|
2928
2990
|
display: flex;
|
|
2929
|
-
|
|
2991
|
+
justify-content: space-between;
|
|
2992
|
+
align-items: center;
|
|
2930
2993
|
border-bottom: 1px solid var(--color-border-primary);
|
|
2931
2994
|
flex-shrink: 0;
|
|
2995
|
+
gap: 16px;
|
|
2932
2996
|
}
|
|
2997
|
+
|
|
2998
|
+
.skills-tabs-left {
|
|
2999
|
+
display: flex;
|
|
3000
|
+
gap: 4px;
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
.skills-tabs-controls {
|
|
3004
|
+
display: flex;
|
|
3005
|
+
align-items: center;
|
|
3006
|
+
gap: 12px;
|
|
3007
|
+
padding-right: 12px;
|
|
3008
|
+
margin-bottom: 4px;
|
|
3009
|
+
}
|
|
3010
|
+
|
|
2933
3011
|
.skills-tab {
|
|
2934
3012
|
background: none;
|
|
2935
3013
|
border: none;
|
|
@@ -2998,34 +3076,40 @@ body {
|
|
|
2998
3076
|
}
|
|
2999
3077
|
|
|
3000
3078
|
/* ── My Skills list ──────────────────────────────────────────────────────── */
|
|
3001
|
-
/* ── Skills Filter
|
|
3002
|
-
.skills-filter-bar {
|
|
3003
|
-
display: flex;
|
|
3004
|
-
align-items: center;
|
|
3005
|
-
justify-content: flex-end;
|
|
3006
|
-
gap: 10px;
|
|
3007
|
-
padding: 10px 0 6px;
|
|
3008
|
-
}
|
|
3009
|
-
|
|
3079
|
+
/* ── Skills Filter Toggle (in tab bar) ───────────────────────────────────── */
|
|
3010
3080
|
.skills-filter-toggle {
|
|
3011
3081
|
display: flex;
|
|
3012
3082
|
align-items: center;
|
|
3013
|
-
gap:
|
|
3083
|
+
gap: 8px;
|
|
3014
3084
|
cursor: pointer;
|
|
3015
3085
|
user-select: none;
|
|
3086
|
+
padding: 0;
|
|
3087
|
+
background: none;
|
|
3088
|
+
border: none;
|
|
3016
3089
|
}
|
|
3017
3090
|
|
|
3018
3091
|
.skills-filter-toggle input[type="checkbox"] {
|
|
3019
3092
|
display: none;
|
|
3020
3093
|
}
|
|
3021
3094
|
|
|
3095
|
+
.skills-filter-label {
|
|
3096
|
+
font-size: 13px;
|
|
3097
|
+
color: var(--color-text-secondary);
|
|
3098
|
+
white-space: nowrap;
|
|
3099
|
+
transition: color 0.15s;
|
|
3100
|
+
}
|
|
3101
|
+
|
|
3102
|
+
.skills-filter-toggle:hover .skills-filter-label {
|
|
3103
|
+
color: var(--color-text-primary);
|
|
3104
|
+
}
|
|
3105
|
+
|
|
3022
3106
|
/* Mini toggle track — matches the skill-card toggle style */
|
|
3023
3107
|
.skills-filter-toggle-track {
|
|
3024
3108
|
position: relative;
|
|
3025
|
-
width:
|
|
3026
|
-
height:
|
|
3109
|
+
width: 32px;
|
|
3110
|
+
height: 18px;
|
|
3027
3111
|
background: var(--color-border-primary);
|
|
3028
|
-
border-radius:
|
|
3112
|
+
border-radius: 9px;
|
|
3029
3113
|
flex-shrink: 0;
|
|
3030
3114
|
transition: background 0.18s;
|
|
3031
3115
|
}
|
|
@@ -3034,8 +3118,8 @@ body {
|
|
|
3034
3118
|
position: absolute;
|
|
3035
3119
|
top: 2px;
|
|
3036
3120
|
left: 2px;
|
|
3037
|
-
width:
|
|
3038
|
-
height:
|
|
3121
|
+
width: 14px;
|
|
3122
|
+
height: 14px;
|
|
3039
3123
|
background: #fff;
|
|
3040
3124
|
border-radius: 50%;
|
|
3041
3125
|
transition: transform 0.18s;
|
|
@@ -3044,12 +3128,7 @@ body {
|
|
|
3044
3128
|
background: var(--color-accent, #6c63ff);
|
|
3045
3129
|
}
|
|
3046
3130
|
.skills-filter-toggle input:checked ~ .skills-filter-toggle-track::after {
|
|
3047
|
-
transform: translateX(
|
|
3048
|
-
}
|
|
3049
|
-
|
|
3050
|
-
.skills-filter-label {
|
|
3051
|
-
font-size: 12px;
|
|
3052
|
-
color: var(--color-text-secondary);
|
|
3131
|
+
transform: translateX(14px);
|
|
3053
3132
|
}
|
|
3054
3133
|
|
|
3055
3134
|
#skills-list {
|
|
@@ -3064,6 +3143,42 @@ body {
|
|
|
3064
3143
|
padding: 20px 0;
|
|
3065
3144
|
text-align: center;
|
|
3066
3145
|
}
|
|
3146
|
+
.skills-empty-text {
|
|
3147
|
+
margin-bottom: 12px;
|
|
3148
|
+
color: var(--color-text-secondary);
|
|
3149
|
+
font-size: 13px;
|
|
3150
|
+
}
|
|
3151
|
+
/* Guided CTA card in the empty-skills state — mirrors .creator-new-card */
|
|
3152
|
+
.skills-empty-create-btn {
|
|
3153
|
+
display: flex;
|
|
3154
|
+
align-items: center;
|
|
3155
|
+
gap: 10px;
|
|
3156
|
+
padding: 14px 16px;
|
|
3157
|
+
background: var(--color-bg-secondary);
|
|
3158
|
+
border: 1px dashed var(--color-border-primary);
|
|
3159
|
+
border-radius: 8px;
|
|
3160
|
+
cursor: pointer;
|
|
3161
|
+
font-size: 13px;
|
|
3162
|
+
font-weight: 500;
|
|
3163
|
+
color: var(--color-text-secondary);
|
|
3164
|
+
transition: border-color .15s, color .15s, background .15s;
|
|
3165
|
+
text-align: left;
|
|
3166
|
+
margin-top: 4px;
|
|
3167
|
+
}
|
|
3168
|
+
.skills-empty-create-btn:hover {
|
|
3169
|
+
border-color: var(--color-accent-primary);
|
|
3170
|
+
color: var(--color-accent-primary);
|
|
3171
|
+
background: var(--color-accent-bg, rgba(59,130,246,0.04));
|
|
3172
|
+
}
|
|
3173
|
+
.skills-empty-create-arrow {
|
|
3174
|
+
margin-left: auto;
|
|
3175
|
+
opacity: 0.5;
|
|
3176
|
+
transition: opacity .15s, transform .15s;
|
|
3177
|
+
}
|
|
3178
|
+
.skills-empty-create-btn:hover .skills-empty-create-arrow {
|
|
3179
|
+
opacity: 1;
|
|
3180
|
+
transform: translateX(3px);
|
|
3181
|
+
}
|
|
3067
3182
|
|
|
3068
3183
|
/* ── Skill Card ──────────────────────────────────────────────────────────── */
|
|
3069
3184
|
.skill-card {
|
|
@@ -3450,24 +3565,44 @@ body {
|
|
|
3450
3565
|
}
|
|
3451
3566
|
|
|
3452
3567
|
/* ── Brand Skills tab ────────────────────────────────────────────────────── */
|
|
3453
|
-
|
|
3454
|
-
display: flex;
|
|
3455
|
-
justify-content: flex-end;
|
|
3456
|
-
padding: 8px 0 4px;
|
|
3457
|
-
}
|
|
3568
|
+
/* Refresh button now lives in skills-tabs-controls, but keep these styles */
|
|
3458
3569
|
.btn-brand-skills-refresh {
|
|
3459
|
-
|
|
3570
|
+
display: flex;
|
|
3571
|
+
align-items: center;
|
|
3572
|
+
gap: 6px;
|
|
3573
|
+
background: var(--color-bg-secondary);
|
|
3460
3574
|
border: 1px solid var(--color-border-primary);
|
|
3461
3575
|
border-radius: 6px;
|
|
3462
3576
|
color: var(--color-text-secondary);
|
|
3463
3577
|
cursor: pointer;
|
|
3464
|
-
font-size:
|
|
3465
|
-
padding:
|
|
3466
|
-
transition: color .15s, border-color .15s;
|
|
3578
|
+
font-size: 13px;
|
|
3579
|
+
padding: 6px 12px;
|
|
3580
|
+
transition: color .15s, background .15s, border-color .15s;
|
|
3581
|
+
white-space: nowrap;
|
|
3467
3582
|
}
|
|
3468
3583
|
.btn-brand-skills-refresh:hover {
|
|
3584
|
+
background: var(--color-bg-hover);
|
|
3469
3585
|
border-color: var(--color-accent-primary);
|
|
3470
|
-
color: var(--color-
|
|
3586
|
+
color: var(--color-text-primary);
|
|
3587
|
+
}
|
|
3588
|
+
.btn-brand-skills-refresh:disabled {
|
|
3589
|
+
opacity: 0.6;
|
|
3590
|
+
cursor: not-allowed;
|
|
3591
|
+
}
|
|
3592
|
+
.btn-brand-skills-refresh svg {
|
|
3593
|
+
flex-shrink: 0;
|
|
3594
|
+
}
|
|
3595
|
+
.btn-brand-skills-refresh svg.spinning {
|
|
3596
|
+
animation: spin 1s linear infinite;
|
|
3597
|
+
}
|
|
3598
|
+
|
|
3599
|
+
@keyframes spin {
|
|
3600
|
+
from {
|
|
3601
|
+
transform: rotate(0deg);
|
|
3602
|
+
}
|
|
3603
|
+
to {
|
|
3604
|
+
transform: rotate(360deg);
|
|
3605
|
+
}
|
|
3471
3606
|
}
|
|
3472
3607
|
|
|
3473
3608
|
#brand-skills-list {
|
|
@@ -4686,14 +4821,14 @@ body.setup-mode[data-theme="dark"] {
|
|
|
4686
4821
|
#channels-body {
|
|
4687
4822
|
flex: 1;
|
|
4688
4823
|
overflow-y: auto;
|
|
4689
|
-
padding:
|
|
4824
|
+
padding: 28px 36px;
|
|
4690
4825
|
display: flex;
|
|
4691
4826
|
flex-direction: column;
|
|
4692
|
-
gap:
|
|
4827
|
+
gap: 20px;
|
|
4693
4828
|
}
|
|
4694
4829
|
|
|
4695
4830
|
.channels-page-header {
|
|
4696
|
-
margin-bottom:
|
|
4831
|
+
margin-bottom: 4px;
|
|
4697
4832
|
}
|
|
4698
4833
|
|
|
4699
4834
|
.channels-page-title {
|
|
@@ -4729,14 +4864,20 @@ body.setup-mode[data-theme="dark"] {
|
|
|
4729
4864
|
animation: section-flash 2.4s ease-in-out;
|
|
4730
4865
|
}
|
|
4731
4866
|
/* ── Channel card ───────────────────────────────────────────────────────── */
|
|
4867
|
+
#channels-list {
|
|
4868
|
+
display: flex;
|
|
4869
|
+
flex-direction: column;
|
|
4870
|
+
gap: 16px;
|
|
4871
|
+
}
|
|
4872
|
+
|
|
4732
4873
|
.channel-card {
|
|
4733
4874
|
background: var(--color-bg-secondary);
|
|
4734
4875
|
border: 1px solid var(--color-border-primary);
|
|
4735
4876
|
border-radius: 12px;
|
|
4736
|
-
padding: 20px
|
|
4877
|
+
padding: 16px 20px;
|
|
4737
4878
|
display: flex;
|
|
4738
4879
|
flex-direction: column;
|
|
4739
|
-
gap:
|
|
4880
|
+
gap: 12px;
|
|
4740
4881
|
}
|
|
4741
4882
|
|
|
4742
4883
|
.channel-card-header {
|
|
@@ -4816,7 +4957,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
4816
4957
|
|
|
4817
4958
|
/* ── Channel card body ───────────────────────────────────────────────────── */
|
|
4818
4959
|
.channel-card-body {
|
|
4819
|
-
padding:
|
|
4960
|
+
padding: 0;
|
|
4820
4961
|
}
|
|
4821
4962
|
|
|
4822
4963
|
.channel-status-hint {
|
|
@@ -4837,7 +4978,7 @@ body.setup-mode[data-theme="dark"] {
|
|
|
4837
4978
|
align-items: center;
|
|
4838
4979
|
justify-content: flex-end;
|
|
4839
4980
|
gap: 12px;
|
|
4840
|
-
padding-top:
|
|
4981
|
+
padding-top: 12px;
|
|
4841
4982
|
border-top: 1px solid var(--color-border-primary);
|
|
4842
4983
|
}
|
|
4843
4984
|
|
|
@@ -5799,6 +5940,61 @@ body.setup-mode[data-theme="dark"] {
|
|
|
5799
5940
|
white-space: nowrap;
|
|
5800
5941
|
}
|
|
5801
5942
|
|
|
5943
|
+
/* ── Promo banner (non-licensed users) ─────────────────────────────────── */
|
|
5944
|
+
#creator-promo-banner {
|
|
5945
|
+
display: flex;
|
|
5946
|
+
align-items: center;
|
|
5947
|
+
gap: 8px;
|
|
5948
|
+
padding: 10px 14px;
|
|
5949
|
+
background: var(--color-bg-secondary);
|
|
5950
|
+
border: 1px solid var(--color-border-primary);
|
|
5951
|
+
border-radius: 8px;
|
|
5952
|
+
font-size: 12.5px;
|
|
5953
|
+
color: var(--color-text-secondary);
|
|
5954
|
+
}
|
|
5955
|
+
#creator-promo-banner svg {
|
|
5956
|
+
flex-shrink: 0;
|
|
5957
|
+
color: var(--color-accent-primary);
|
|
5958
|
+
opacity: 0.8;
|
|
5959
|
+
}
|
|
5960
|
+
#creator-promo-banner a {
|
|
5961
|
+
margin-left: auto;
|
|
5962
|
+
white-space: nowrap;
|
|
5963
|
+
color: var(--color-accent-primary);
|
|
5964
|
+
text-decoration: none;
|
|
5965
|
+
font-weight: 500;
|
|
5966
|
+
}
|
|
5967
|
+
#creator-promo-banner a:hover {
|
|
5968
|
+
text-decoration: underline;
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5971
|
+
/* ── Cloud skills lock (non-licensed users) ─────────────────────────────── */
|
|
5972
|
+
#creator-cloud-lock {
|
|
5973
|
+
display: flex;
|
|
5974
|
+
align-items: center;
|
|
5975
|
+
gap: 8px;
|
|
5976
|
+
padding: 12px 14px;
|
|
5977
|
+
border: 1px dashed var(--color-border-primary);
|
|
5978
|
+
border-radius: 8px;
|
|
5979
|
+
font-size: 12.5px;
|
|
5980
|
+
color: var(--color-text-muted);
|
|
5981
|
+
}
|
|
5982
|
+
#creator-cloud-lock svg {
|
|
5983
|
+
flex-shrink: 0;
|
|
5984
|
+
opacity: 0.5;
|
|
5985
|
+
}
|
|
5986
|
+
#creator-cloud-lock a {
|
|
5987
|
+
margin-left: auto;
|
|
5988
|
+
white-space: nowrap;
|
|
5989
|
+
color: var(--color-accent-primary);
|
|
5990
|
+
text-decoration: none;
|
|
5991
|
+
font-weight: 500;
|
|
5992
|
+
opacity: 0.85;
|
|
5993
|
+
}
|
|
5994
|
+
#creator-cloud-lock a:hover {
|
|
5995
|
+
text-decoration: underline;
|
|
5996
|
+
}
|
|
5997
|
+
|
|
5802
5998
|
/* ── Create New Skill entry card ───────────────────────────────────────── */
|
|
5803
5999
|
.creator-new-card {
|
|
5804
6000
|
display: flex;
|
data/lib/clacky/web/app.js
CHANGED
|
@@ -149,6 +149,9 @@ const Router = (() => {
|
|
|
149
149
|
Sessions.updateInfoBar(s);
|
|
150
150
|
Sessions._restoreMessagesPublic(id);
|
|
151
151
|
Sessions._setActiveId(id);
|
|
152
|
+
// Immediately re-attach saved progress UI (timer + spinner) so it appears
|
|
153
|
+
// instantly without waiting for the async history fetch or WS replay.
|
|
154
|
+
Sessions._attachProgressUI(id);
|
|
152
155
|
WS.setSubscribedSession(id);
|
|
153
156
|
// Only disable send button until server confirms subscription
|
|
154
157
|
// Input field remains usable so user can type while waiting
|
|
@@ -310,7 +313,7 @@ WS.onEvent(ev => {
|
|
|
310
313
|
banner.textContent = I18n.t("offline.banner");
|
|
311
314
|
banner.style.display = "block";
|
|
312
315
|
}
|
|
313
|
-
Sessions.
|
|
316
|
+
Sessions.clearAllProgress();
|
|
314
317
|
Sessions.updateStatusBar("idle");
|
|
315
318
|
break;
|
|
316
319
|
}
|
|
@@ -452,8 +455,8 @@ WS.onEvent(ev => {
|
|
|
452
455
|
if (ev.phase === "active" || ev.status === "start") {
|
|
453
456
|
const progress_type = ev.progress_type || "thinking";
|
|
454
457
|
const metadata = ev.metadata || {};
|
|
455
|
-
console.log("[DEBUG] calling showProgress:", { message: ev.message, progress_type, metadata });
|
|
456
|
-
Sessions.showProgress(ev.message, progress_type, metadata);
|
|
458
|
+
console.log("[DEBUG] calling showProgress:", { message: ev.message, progress_type, metadata, started_at: ev.started_at });
|
|
459
|
+
Sessions.showProgress(ev.message, progress_type, metadata, ev.started_at || null);
|
|
457
460
|
} else {
|
|
458
461
|
console.log("[DEBUG] calling clearProgress:", ev.message);
|
|
459
462
|
Sessions.clearProgress(ev.message);
|
|
@@ -467,6 +470,11 @@ WS.onEvent(ev => {
|
|
|
467
470
|
Sessions.appendInfo(`✓ ${I18n.t("chat.done", { n: ev.iterations, cost: (ev.cost || 0).toFixed(4) })}`);
|
|
468
471
|
break;
|
|
469
472
|
|
|
473
|
+
case "request_feedback":
|
|
474
|
+
if (ev.session_id !== Sessions.activeId) break;
|
|
475
|
+
Sessions.showFeedbackRequest(ev.question, ev.context, ev.options);
|
|
476
|
+
break;
|
|
477
|
+
|
|
470
478
|
case "request_confirmation":
|
|
471
479
|
if (ev.session_id !== Sessions.activeId) break;
|
|
472
480
|
showConfirmModal(ev.id, ev.message);
|