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.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +38 -0
  3. data/lib/clacky/agent/llm_caller.rb +5 -5
  4. data/lib/clacky/agent/memory_updater.rb +1 -1
  5. data/lib/clacky/agent/session_serializer.rb +2 -1
  6. data/lib/clacky/agent/skill_auto_creator.rb +119 -0
  7. data/lib/clacky/agent/skill_evolution.rb +46 -0
  8. data/lib/clacky/agent/skill_manager.rb +8 -0
  9. data/lib/clacky/agent/skill_reflector.rb +97 -0
  10. data/lib/clacky/agent.rb +38 -12
  11. data/lib/clacky/agent_config.rb +10 -1
  12. data/lib/clacky/brand_config.rb +23 -0
  13. data/lib/clacky/cli.rb +1 -1
  14. data/lib/clacky/default_skills/onboard/SKILL.md +15 -7
  15. data/lib/clacky/default_skills/personal-website/publish.rb +1 -1
  16. data/lib/clacky/default_skills/skill-creator/SKILL.md +46 -0
  17. data/lib/clacky/json_ui_controller.rb +0 -4
  18. data/lib/clacky/message_history.rb +0 -12
  19. data/lib/clacky/plain_ui_controller.rb +19 -1
  20. data/lib/clacky/platform_http_client.rb +2 -4
  21. data/lib/clacky/providers.rb +12 -1
  22. data/lib/clacky/server/channel/channel_ui_controller.rb +0 -2
  23. data/lib/clacky/server/http_server.rb +13 -1
  24. data/lib/clacky/server/web_ui_controller.rb +55 -29
  25. data/lib/clacky/tools/shell.rb +91 -170
  26. data/lib/clacky/ui2/ui_controller.rb +100 -93
  27. data/lib/clacky/ui_interface.rb +0 -1
  28. data/lib/clacky/utils/arguments_parser.rb +5 -2
  29. data/lib/clacky/utils/limit_stack.rb +81 -13
  30. data/lib/clacky/version.rb +1 -1
  31. data/lib/clacky/web/app.css +247 -51
  32. data/lib/clacky/web/app.js +11 -3
  33. data/lib/clacky/web/brand.js +21 -3
  34. data/lib/clacky/web/creator.js +13 -2
  35. data/lib/clacky/web/i18n.js +41 -15
  36. data/lib/clacky/web/index.html +38 -20
  37. data/lib/clacky/web/sessions.js +256 -57
  38. data/lib/clacky/web/settings.js +32 -0
  39. data/lib/clacky/web/skills.js +61 -1
  40. 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 StandardError, error_msg
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 < StandardError
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 size limit is exceeded
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 = max_size
12
- @items = []
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
- @items << element
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| push(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
- if @items.size > @max_size
78
- # Remove oldest elements, keep only the latest max_size items
79
- @items.shift(@items.size - @max_size)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "0.9.30"
4
+ VERSION = "0.9.32"
5
5
  end
@@ -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: 4px 8px 8px; min-height: 108px; }
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-bg-hover);
352
- color: var(--color-text-secondary);
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-accent-primary);
362
- color: #fff;
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 var(--color-border-primary);
372
+ border-left: 1px solid rgba(255,255,255,0.3);
369
373
  border-radius: 0 5px 5px 0;
370
- background: var(--color-bg-hover);
371
- color: var(--color-text-tertiary);
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-accent-primary);
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
- gap: 4px;
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 Bar ───────────────────────────────────────────────────── */
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: 7px;
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: 28px;
3026
- height: 16px;
3109
+ width: 32px;
3110
+ height: 18px;
3027
3111
  background: var(--color-border-primary);
3028
- border-radius: 8px;
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: 12px;
3038
- height: 12px;
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(12px);
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
- #brand-skills-header {
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
- background: none;
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: 12px;
3465
- padding: 4px 10px;
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-accent-primary);
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: 32px 40px;
4824
+ padding: 28px 36px;
4690
4825
  display: flex;
4691
4826
  flex-direction: column;
4692
- gap: 24px;
4827
+ gap: 20px;
4693
4828
  }
4694
4829
 
4695
4830
  .channels-page-header {
4696
- margin-bottom: 8px;
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 24px;
4877
+ padding: 16px 20px;
4737
4878
  display: flex;
4738
4879
  flex-direction: column;
4739
- gap: 18px;
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: 2px 0;
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: 4px;
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;
@@ -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.clearProgress();
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);